From 1a084ef1ad05d05cdb150d1116b01853f012264e Mon Sep 17 00:00:00 2001 From: Florian Rohr Date: Sat, 13 Dec 2025 12:37:18 +0100 Subject: [PATCH 1/6] added security_features --- securety_api/.env | 4 + securety_api/pom.xml | 113 ++ securety_api/src/.DS_Store | Bin 0 -> 6148 bytes securety_api/src/main/.DS_Store | Bin 0 -> 6148 bytes securety_api/src/main/java/.DS_Store | Bin 0 -> 6148 bytes securety_api/src/main/java/com/.DS_Store | Bin 0 -> 6148 bytes .../SecureUseApiApplication.java | 11 + .../CustomAuthenticationEntryPoint.java | 22 + .../exceptions/CustomExceptionHandler.java | 36 + .../exceptions/ExceptionResponse.java | 58 + .../secure_use_api/exceptions/Exceptions.java | 21 + .../secure_use_api/model/ApiTokenModel.java | 71 + .../model/RefreshTokenModel.java | 50 + .../model/UserAdditionsModel.java | 57 + .../secure_use_api/model/UserComposite.java | 28 + .../secure_use_api/model/UserMetaModel.java | 80 + .../model/UserSecurityModel.java | 65 + .../repository/ApiTokenRepository.java | 18 + .../repository/RefreshTokenRepository.java | 20 + .../repository/UserAdditionsRepository.java | 25 + .../repository/UserMetaRepository.java | 14 + .../repository/UserSecurityRepository.java | 12 + .../rest/controller/ApiTokenController.java | 73 + .../controller/AuthenticationController.java | 132 ++ .../rest/controller/UserController.java | 48 + .../secure_use_api/rest/dto/IdActionDto.java | 28 + .../rest/dto/RefreshTokenDto.java | 21 + .../rest/dto/ResourceLimitLeftDto.java | 30 + .../secure_use_api/rest/dto/TokensDto.java | 26 + .../secure_use_api/rest/dto/UserLoginDto.java | 33 + .../rest/dto/UserRegistrationDto.java | 28 + .../rest/middleware/AuditLogMiddleware.java | 83 + .../middleware/AuthenticationMiddleware.java | 83 + .../middleware/AuthorizationMiddleware.java | 73 + .../ExceptionHandlerMiddleware.java | 26 + .../rest/middleware/RateLimitMiddleware.java | 45 + .../middleware/ResourceLimitMiddleware.java | 92 + .../rest/service/ApiTokenService.java | 68 + .../rest/service/AuthenticationService.java | 87 + .../rest/service/RefreshTokenService.java | 59 + .../rest/service/UserService.java | 92 + .../security/CustomAuthProvider.java | 71 + .../secure_use_api/security/RequireScope.java | 12 + .../com/secure_use_api/security/Role.java | 73 + .../security/SecurityConfig.java | 72 + .../security/UserAuthentication.java | 76 + .../security/UserDetailsImpl.java | 64 + .../secure_use_api/security/WebConfig.java | 43 + .../secure_use_api/token/AbstractToken.java | 12 + .../com/secure_use_api/token/AccessToken.java | 133 ++ .../com/secure_use_api/token/ApiToken.java | 37 + .../com/secure_use_api/token/JwtHandler.java | 47 + .../secure_use_api/token/PasetoHandler.java | 19 + .../secure_use_api/token/RefreshToken.java | 95 + .../token/SimpleTokenStringHandler.java | 9 + .../java/com/secure_use_api/util/Config.java | 36 + .../secure_use_api/util/HibernateUtil.java | 25 + securety_api/src/main/resources/.DS_Store | Bin 0 -> 6148 bytes .../src/main/resources/application.properties | 11 + .../src/main/resources/logback-spring.xml | 29 + .../src/main/resources/templates/hello.html | 12 + securety_api/src/test/.DS_Store | Bin 0 -> 8196 bytes securety_api/src/test/java/.DS_Store | Bin 0 -> 6148 bytes securety_api/src/test/java/com/.DS_Store | Bin 0 -> 6148 bytes .../SecureUseAppiApplicationTests.java | 13 + securety_api/src/test/postman/.DS_Store | Bin 0 -> 6148 bytes ...Ba.ApiSecurityTest.postman_collection.json | 1551 ++++++++++++++ ...i.Ba.ApiSecurityTest.postman_test_run.json | 1784 +++++++++++++++++ securety_api/src/test/resources/.gitkeep | 0 69 files changed, 6056 insertions(+) create mode 100644 securety_api/.env create mode 100644 securety_api/pom.xml create mode 100644 securety_api/src/.DS_Store create mode 100644 securety_api/src/main/.DS_Store create mode 100644 securety_api/src/main/java/.DS_Store create mode 100644 securety_api/src/main/java/com/.DS_Store create mode 100644 securety_api/src/main/java/com/secure_use_api/SecureUseApiApplication.java create mode 100644 securety_api/src/main/java/com/secure_use_api/exceptions/CustomAuthenticationEntryPoint.java create mode 100644 securety_api/src/main/java/com/secure_use_api/exceptions/CustomExceptionHandler.java create mode 100644 securety_api/src/main/java/com/secure_use_api/exceptions/ExceptionResponse.java create mode 100644 securety_api/src/main/java/com/secure_use_api/exceptions/Exceptions.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/ApiTokenModel.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/RefreshTokenModel.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/UserAdditionsModel.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/UserComposite.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/UserMetaModel.java create mode 100644 securety_api/src/main/java/com/secure_use_api/model/UserSecurityModel.java create mode 100644 securety_api/src/main/java/com/secure_use_api/repository/ApiTokenRepository.java create mode 100644 securety_api/src/main/java/com/secure_use_api/repository/RefreshTokenRepository.java create mode 100644 securety_api/src/main/java/com/secure_use_api/repository/UserAdditionsRepository.java create mode 100644 securety_api/src/main/java/com/secure_use_api/repository/UserMetaRepository.java create mode 100644 securety_api/src/main/java/com/secure_use_api/repository/UserSecurityRepository.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/controller/ApiTokenController.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/controller/AuthenticationController.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/controller/UserController.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/IdActionDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/RefreshTokenDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/ResourceLimitLeftDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/TokensDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/UserLoginDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/dto/UserRegistrationDto.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/AuditLogMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthenticationMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthorizationMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/ExceptionHandlerMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/RateLimitMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/middleware/ResourceLimitMiddleware.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/service/ApiTokenService.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/service/AuthenticationService.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/service/RefreshTokenService.java create mode 100644 securety_api/src/main/java/com/secure_use_api/rest/service/UserService.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/CustomAuthProvider.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/RequireScope.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/Role.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/SecurityConfig.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/UserAuthentication.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/UserDetailsImpl.java create mode 100644 securety_api/src/main/java/com/secure_use_api/security/WebConfig.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/AbstractToken.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/AccessToken.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/ApiToken.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/JwtHandler.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/PasetoHandler.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/RefreshToken.java create mode 100644 securety_api/src/main/java/com/secure_use_api/token/SimpleTokenStringHandler.java create mode 100644 securety_api/src/main/java/com/secure_use_api/util/Config.java create mode 100644 securety_api/src/main/java/com/secure_use_api/util/HibernateUtil.java create mode 100644 securety_api/src/main/resources/.DS_Store create mode 100644 securety_api/src/main/resources/application.properties create mode 100644 securety_api/src/main/resources/logback-spring.xml create mode 100644 securety_api/src/main/resources/templates/hello.html create mode 100644 securety_api/src/test/.DS_Store create mode 100644 securety_api/src/test/java/.DS_Store create mode 100644 securety_api/src/test/java/com/.DS_Store create mode 100644 securety_api/src/test/java/com/secure_use_api/SecureUseAppiApplicationTests.java create mode 100644 securety_api/src/test/postman/.DS_Store create mode 100644 securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_collection.json create mode 100644 securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_test_run.json create mode 100644 securety_api/src/test/resources/.gitkeep diff --git a/securety_api/.env b/securety_api/.env new file mode 100644 index 0000000..5613441 --- /dev/null +++ b/securety_api/.env @@ -0,0 +1,4 @@ +SECRET_ACCESS_TOKEN_KEY=AuthSecretKey +SECRET_REFRESH_TOKEN_KEY=RefreshTokenKey +RATE_LIMIT_PER_SECOND=1 +RESOURCE_LIMIT_PER_DAY=60 \ No newline at end of file diff --git a/securety_api/pom.xml b/securety_api/pom.xml new file mode 100644 index 0000000..2564cec --- /dev/null +++ b/securety_api/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + + com.secure_use_api + secure_use_api + 1.0 + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + + UTF-8 + com.secure_use_api.App + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.security + spring-security-test + test + + + + de.mkammerer + argon2-jvm + 2.7 + + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + + + com.google.guava + guava + 32.1.3-jre + + + + + com.auth0 + java-jwt + 4.2.1 + + + + + io.github.cdimascio + dotenv-java + 2.0.0 + + + + + com.google.code.gson + gson + 2.13.2 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/securety_api/src/.DS_Store b/securety_api/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..59eccef51c643d253447e0291a58caabc1ee894c GIT binary patch literal 6148 zcmeHK%}*0S6n|5yY(ebGhkRVzq%rY8v;y)G4z2}5h*1b72m!3S?NB#tcbeTT6$nW$ zUi9Q2;2+@0oAH0}ZsNs@R}UV&>6?!NEqXFhW9B6@zc=sAd$aSK*_|B#Ah}xUCV&Y5 z1{RLVF06hc+|SDyNhk|<5}sqxXGPa%F1C3>Et&z%z`xA^e|M8m0S7z?+TPzg=)3-1 zh?vB2KerxL)qDQVt@>URXR{wjq9b|ke5cWAbQ|0GC#;^2owyd|oN$x3RvGi%rtY|F z9<3M5{y7%Jju+5S1b8KiUA7*2L5bCKtQM3a5u29|BW0ut=Fs8ct?9{8YkYd_FU3V@Vg|WM`06QpmIE)y?hw>EGVOm_fJ9$6e5Th zR+h81C=P|~0diD9`ze1LUvJRY%9qOXxA~fwLXO79r+%9+H6O3Ir`Df5Yp(yN=K80) zx-axxOq-X|z3INb{sD7vXt-eZxlVDn9P%}tV?GOWevKB)bc8!&vB-kuknZ7*SdZz` zIPV>xtG6v+g_a(jm{#|z*r)%*Ad)VGlm?sS&?_zN>_jwPKc#PMiLqi@Sqyd<=I7%r zRb!6@(HadS#BJYkslOEDSRAv;h7*QPP^P*4bq}AAKb1(AuR299C}(be#pOmxW$bHG zGa(}tJTFa>S`C~E&DhJ*ma*?CrvX=C7Vg6u?7{)Ogjety-oktM2%q5#d?SP;NH^&r zm&qU*CL`nuQTY?i%{5iM>&v=ufaq6LM>P(&F@)D?rMn*)*I=(kn>hC~?-M2w7i z)X0SY$e<8)@gNp&VcQm literal 0 HcmV?d00001 diff --git a/securety_api/src/main/.DS_Store b/securety_api/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ad59c1c3ddc5d140a74089475f0db205d7db3353 GIT binary patch literal 6148 zcmeHK&o2W(6n?{EOC&vzIBw$FP`?`DvWkPN6%I)4YQwU2q}%GDX|iYj7*~IZe~0hQ z458{sXyzqvzBg~)o89@6`SFNIRNM6>q9PHwP)27G%>dzX)-N(*JmWyYbL=JZJW6DQ zK2K=HGGH0_+YIpDb?Ayhx}&gf|88My;YEpB#Cu%S$!^U)e~NCCIBS*5&thcM85^H) zC)_FbG`N(VpcS^-Sv71PbMHXPBzm*M=rC?{YQ>ptnYO|>Z8TLvTyG%c znKt+a4=~=>a~CwzM5Y(;aeRTA)S!$cwPBsJ-N(!$qCPS+AY({48nCiHwm^lIf60K^ z8}8J<{CoS;mI2GaPcp#gg9l~w4dxov(Sb&l06+(BC9u`gALy|I=o`#6!UGXH6{u5% zDKUhp9E48CxNqWfjXIr#`ZDG*Ulyi95vK4Eo(d=7YqYIpz%uZefl=Kp^8LU2ef|GA z$@VM*mVu#SKsYtfA<{IIF P*oT0k!8VqGUuEDGBA=rl literal 0 HcmV?d00001 diff --git a/securety_api/src/main/java/.DS_Store b/securety_api/src/main/java/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2120347adc86205738e61cb8696666c669928448 GIT binary patch literal 6148 zcmeHK!AiqG5Z!I7O(;SS3OxqA7OhrGi8ceg&q~=fxIqMJkCH{`i z>~4foy?78TGcfx$vojm!ZP>{$#<(*L_86-(#tcxzk_p2%g6pVDQqq<(Ajdt#(2s-< zPq-b)e_#O5uFQr^u<=>6aDKwEs5fS_!)~W(wY$A})0*}6TTSbr-I>o#V{3c&=zQ=Pjbrhmi0AN4O4-m@z$;92 zHqY)PjzoM9mcqO6A|wWg0b*cv8PGSLQCr;gw)W2zs&K~Tn_(aQX0AIFu3Hao zNq5F=g)|Za#K0m0*!zLhvHqX_-2Y1_8i)a6U^N-wmA>0|AU9oGm$JlKYk}T@qM%={ m@GAr+vJ`_amf{Ae6tG)30kk#d3c&(GKLU~l8i;{EW#AL?j#AwK literal 0 HcmV?d00001 diff --git a/securety_api/src/main/java/com/.DS_Store b/securety_api/src/main/java/com/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6be1c0d5bbbc408f205de4056d5e5bbbb8784026 GIT binary patch literal 6148 zcmeHK!A{#i5SI%7IqKCQ)l)yiuHxLlntp_#wW8 z-+?!~8$?A*Z!N9nNwaUfJF~XmTDu-1Qk_xvJyDa00vL0xf#xURakgiu`JOqjvU3cm zM=_141PlHJ1$gb=+4VZdx;Y_#@ect=(I|`%P{-F8e5m(m(`h4EUR~cg ziZAlZ!d%<8AcgPO;;rWy-N1QKpQ~t4HSB~x0fyR~q;5xc3;PY=H=V;HN=gKig0OeA_E>-ywL%DSHYabVS zt{l5`Qoi_5{?5u*D9YX)^J^PUDs(KZC?E>_r~ub~K$Fk^gJ0MGHb@##Kos~t6;Sp4 zXupGZ^JnX`x8$={!_Hs~j;kC$rogdBF=F{BUV&MlU$X-aJy(v=1IUknl|dR&;EyWs E0A$r*rvLx| literal 0 HcmV?d00001 diff --git a/securety_api/src/main/java/com/secure_use_api/SecureUseApiApplication.java b/securety_api/src/main/java/com/secure_use_api/SecureUseApiApplication.java new file mode 100644 index 0000000..734c39e --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/SecureUseApiApplication.java @@ -0,0 +1,11 @@ +package com.secure_use_api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SecureUseApiApplication { + public static void main(String[] args) { + SpringApplication.run(SecureUseApiApplication.class, args); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/exceptions/CustomAuthenticationEntryPoint.java b/securety_api/src/main/java/com/secure_use_api/exceptions/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..a7ea4e2 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/exceptions/CustomAuthenticationEntryPoint.java @@ -0,0 +1,22 @@ +package com.secure_use_api.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + + ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.UNAUTHORIZED, null, ""); + + exceptionResponse.writeToResponse(response, request); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/exceptions/CustomExceptionHandler.java b/securety_api/src/main/java/com/secure_use_api/exceptions/CustomExceptionHandler.java new file mode 100644 index 0000000..5e8791c --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/exceptions/CustomExceptionHandler.java @@ -0,0 +1,36 @@ +package com.secure_use_api.exceptions; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class CustomExceptionHandler extends ResponseEntityExceptionHandler { + + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatusCode status, WebRequest request) { + + Map errors = new HashMap<>(); + + ex.getBindingResult().getFieldErrors() + .forEach(error -> errors.put(error.getField(), error.getDefaultMessage())); + + ExceptionResponse exceptionResponse = new ExceptionResponse(HttpStatus.BAD_REQUEST, errors, + request.getDescription(false)); + + System.out.println(exceptionResponse); + + // Using ExceptionResponseRecord here because else the whole stack trace of parent class RuntimeError is included which would give a tone of server information to the client + return new ResponseEntity<>(exceptionResponse.toExceptionResponseRecord(), + exceptionResponse.getHttpStatus()); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/exceptions/ExceptionResponse.java b/securety_api/src/main/java/com/secure_use_api/exceptions/ExceptionResponse.java new file mode 100644 index 0000000..c1642f9 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/exceptions/ExceptionResponse.java @@ -0,0 +1,58 @@ +package com.secure_use_api.exceptions; + +import java.io.IOException; +import java.time.Instant; +import org.springframework.http.HttpStatus; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class ExceptionResponse extends RuntimeException { + + public record ExceptionResponseRecord(Instant timestamp, String title, int code, Object message, String details) { + } + + private final Instant timestamp; + private final String title; + private final int code; + private final Object message; + private final String details; + + public ExceptionResponse(Instant timestamp, String title, int code, Object message, String details) { + this.timestamp = timestamp; + this.title = title; + this.code = code; + this.message = message; + this.details = details; + } + + public ExceptionResponse(HttpStatus httpStatus, Object message, String details) { + this.timestamp = Instant.now(); + this.title = httpStatus.getReasonPhrase(); + this.code = httpStatus.value(); + this.message = message; + this.details = details; + } + + public HttpStatus getHttpStatus() { + return HttpStatus.valueOf(this.code); + } + + public void writeToResponse(HttpServletResponse response, HttpServletRequest request) throws IOException { + response.setStatus(this.code); + + response.setHeader("content-type", "application/problem+json"); + + response.getWriter().write(String.format( + "{\"timestamp\": \"%s\", \"title\": \"%s\", \"code\": %d, \"message\":\"%s\", \"details\": \"%s\"}", + this.timestamp, this.title, this.code, + this.message, this.details)); + + response.getWriter().flush(); + } + + public ExceptionResponseRecord toExceptionResponseRecord() { + return new ExceptionResponseRecord(this.timestamp, this.title, this.code, this.message, this.details); + } + +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/exceptions/Exceptions.java b/securety_api/src/main/java/com/secure_use_api/exceptions/Exceptions.java new file mode 100644 index 0000000..131a04a --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/exceptions/Exceptions.java @@ -0,0 +1,21 @@ +package com.secure_use_api.exceptions; + +public class Exceptions { + public static class UserAlreadyExists extends RuntimeException { + public UserAlreadyExists(String message) { + super(message); + } + } + + public static class InvalidException extends RuntimeException { + public InvalidException(String message) { + super(message); + } + } + + public static class ExpiredException extends RuntimeException { + public ExpiredException(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/model/ApiTokenModel.java b/securety_api/src/main/java/com/secure_use_api/model/ApiTokenModel.java new file mode 100644 index 0000000..f53d506 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/ApiTokenModel.java @@ -0,0 +1,71 @@ +package com.secure_use_api.model; + +import java.sql.Timestamp; +import java.util.UUID; + +import org.hibernate.annotations.CreationTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "ApiToken") +public class ApiTokenModel { + + @Id + private UUID uid; + + @Lob // Allow larger text storage + @Column(nullable = false) + private String token; + + @Column(nullable = false) + private UUID owner; + + @CreationTimestamp + private Timestamp createdAt; + + @ManyToOne + @JoinColumn(name = "owner", insertable = false, updatable = false) + private UserAdditionsModel userAdditions; + + public ApiTokenModel() { + } + + public UUID getUid() { + return this.uid; + } + + public String getToken() { + return this.token; + } + + public UUID getOwner() { + return this.owner; + } + + public Timestamp getCreatedAt() { + return this.createdAt; + } + + public void setUid(UUID uid) { + this.uid = uid; + } + + public void setToken(String token) { + this.token = token; + } + + public void setOwner(UUID ownerId) { + this.owner = ownerId; + } + + public void setCreatedAt(Timestamp createdAt) { + this.createdAt = createdAt; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/model/RefreshTokenModel.java b/securety_api/src/main/java/com/secure_use_api/model/RefreshTokenModel.java new file mode 100644 index 0000000..e6769c2 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/RefreshTokenModel.java @@ -0,0 +1,50 @@ +package com.secure_use_api.model; + +import java.sql.Timestamp; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "RefreshToken") +public class RefreshTokenModel { + + @Id + private UUID uid; + + @Column(nullable = false, unique = true) + private UUID spouseId; + + @Column(nullable = false) + private Timestamp expireAt; + + public RefreshTokenModel() { + } + + public UUID getUid() { + return this.uid; + } + + public UUID getSpouseId() { + return this.spouseId; + } + + public Timestamp getExpireAt() { + return this.expireAt; + } + + public void setUid(UUID uid) { + this.uid = uid; + } + + public void setSpouse(UUID spouseId) { + this.spouseId = spouseId; + } + + public void setExpireAt(Timestamp expireAt) { + this.expireAt = expireAt; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/model/UserAdditionsModel.java b/securety_api/src/main/java/com/secure_use_api/model/UserAdditionsModel.java new file mode 100644 index 0000000..5b52d04 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/UserAdditionsModel.java @@ -0,0 +1,57 @@ +package com.secure_use_api.model; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "UserAdditions") +public class UserAdditionsModel { + @Id + private UUID userId; + + @OneToOne + @MapsId + @JoinColumn(name = "userId") + private UserMetaModel userMeta; + + @OneToMany(mappedBy = "owner", cascade = CascadeType.ALL) + private List items = new ArrayList(); + + private int resourceTimeUsed; + + private Timestamp resourceTimeUsedLastChangedAt; + + public UserAdditionsModel() { + } + + public int getResourceLimitUsed() { + return this.resourceTimeUsed; + } + + public Timestamp getResourceTimeUsedLastChangedAt() { + return this.resourceTimeUsedLastChangedAt; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public void setResourceLimitUsed(int resourceTimeUsed) { + this.resourceTimeUsed = resourceTimeUsed; + } + + public void setUserMeta(UserMetaModel userMetaModel) { + this.userMeta = userMetaModel; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/model/UserComposite.java b/securety_api/src/main/java/com/secure_use_api/model/UserComposite.java new file mode 100644 index 0000000..4390428 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/UserComposite.java @@ -0,0 +1,28 @@ +package com.secure_use_api.model; + +public class UserComposite { + private UserMetaModel userMeta; + private UserSecurityModel userSecurity; + + public UserComposite(UserMetaModel userMeta, UserSecurityModel userSecurity) { + this.userMeta = userMeta; + this.userSecurity = userSecurity; + } + + // Getters and Setters + public UserMetaModel getUserMeta() { + return userMeta; + } + + public void setUserMeta(UserMetaModel userMeta) { + this.userMeta = userMeta; + } + + public UserSecurityModel getUserSecurity() { + return userSecurity; + } + + public void setUserSecurity(UserSecurityModel userSecurity) { + this.userSecurity = userSecurity; + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/model/UserMetaModel.java b/securety_api/src/main/java/com/secure_use_api/model/UserMetaModel.java new file mode 100644 index 0000000..9a47cde --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/UserMetaModel.java @@ -0,0 +1,80 @@ +package com.secure_use_api.model; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "UserMeta") +public class UserMetaModel { + + @Id + @GeneratedValue + private UUID userId; + + @Column(nullable = false, unique = true) + private String email; + + private String provider; + + private String externalId; + + @ElementCollection + private List roles = new ArrayList<>(); + + @CreationTimestamp + private Timestamp createdAt; + + @UpdateTimestamp + private Timestamp lastChangedAt; + + @OneToOne(mappedBy = "userMeta", cascade = CascadeType.ALL) + private UserSecurityModel userSecurityModel; + + @OneToOne(mappedBy = "userMeta", cascade = CascadeType.ALL) + private UserAdditionsModel userAdditionsModel; + + public UserMetaModel() { + } + + public UUID getUserId() { + return this.userId; + } + + public String getEmail() { + return this.email; + } + + public String getProvider() { + return this.provider; + } + + public String getExternalId() { + return this.externalId; + } + + public List getRoles() { + return this.roles; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setRoles(ArrayList roles) { + this.roles = roles; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/model/UserSecurityModel.java b/securety_api/src/main/java/com/secure_use_api/model/UserSecurityModel.java new file mode 100644 index 0000000..271d735 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/model/UserSecurityModel.java @@ -0,0 +1,65 @@ +package com.secure_use_api.model; + +import java.sql.Timestamp; +import java.util.UUID; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "UserSecurity") +public class UserSecurityModel { + + @Id + private UUID userId; + + @OneToOne + @MapsId + @JoinColumn(name = "UserId") + private UserMetaModel userMeta; + + private String hashedSaltedPassword; + + private int failedLogInAttempts; + + private Timestamp LastLogInAt; + + public UserSecurityModel() { + } + + public String getPassword() { + return this.hashedSaltedPassword; + } + + public int getFailedLogInAttempts() { + return this.failedLogInAttempts; + } + + public Timestamp getLastLogInAt() { + return this.LastLogInAt; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public void setUserMeta(UserMetaModel userMeta) { + this.userMeta = userMeta; + } + + public void setPassword(String password) { + this.hashedSaltedPassword = password; + } + + public void setFailedLogInAttempts(int failedLogInAttempt) { + this.failedLogInAttempts = failedLogInAttempt; + } + + public void setLastLogInAt(Timestamp lastLogInAt) { + this.LastLogInAt = lastLogInAt; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/repository/ApiTokenRepository.java b/securety_api/src/main/java/com/secure_use_api/repository/ApiTokenRepository.java new file mode 100644 index 0000000..5af86d4 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/repository/ApiTokenRepository.java @@ -0,0 +1,18 @@ +package com.secure_use_api.repository; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.secure_use_api.model.ApiTokenModel; + +@Repository +public interface ApiTokenRepository extends JpaRepository { + List findByOwner(UUID ownerId); + + int deleteByUidAndOwner(UUID uid, UUID owner); + + ApiTokenModel findByUidAndOwner(UUID uid, UUID ownerId); +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/repository/RefreshTokenRepository.java b/securety_api/src/main/java/com/secure_use_api/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..773759b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/repository/RefreshTokenRepository.java @@ -0,0 +1,20 @@ +package com.secure_use_api.repository; + +import java.util.Date; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import jakarta.transaction.Transactional; +import com.secure_use_api.model.RefreshTokenModel; + +@Repository +public interface RefreshTokenRepository extends JpaRepository { + @Transactional + @Modifying + @Query(value = "DELETE FROM REFRESH_TOKEN r WHERE r.uid=?1 AND r.spouse_id=?2 AND r.expire_at>?3", nativeQuery = true) + int deleteExpiredToken(UUID uid, UUID spouseId, Date currentTime); +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/repository/UserAdditionsRepository.java b/securety_api/src/main/java/com/secure_use_api/repository/UserAdditionsRepository.java new file mode 100644 index 0000000..9148ef5 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/repository/UserAdditionsRepository.java @@ -0,0 +1,25 @@ +package com.secure_use_api.repository; + +import java.sql.Timestamp; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import jakarta.transaction.Transactional; +import com.secure_use_api.model.UserAdditionsModel; + +@Repository +public interface UserAdditionsRepository extends JpaRepository { + @Transactional + @Query(value = "SELECT * FROM USER_ADDITIONS u WHERE u.user_id=?1 AND u.resource_time_used_last_changed_at BETWEEN ?2 AND ?3", nativeQuery = true) + UserAdditionsModel findByUidAndToday(UUID uid, Timestamp todayStart, Timestamp todayEnd); + + @Transactional + @Modifying + @Query(value = "UPDATE USER_ADDITIONS u SET u.resource_time_used=?2, u.resource_time_used_last_changed_at=?3 WHERE u.user_id=?1", nativeQuery = true) + int UpdateResourceTimeUsedAndLastChangedAtByUid(UUID uid, long resourceTimeUsed, + Timestamp resourceTimeUsedLTimestamp); +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/repository/UserMetaRepository.java b/securety_api/src/main/java/com/secure_use_api/repository/UserMetaRepository.java new file mode 100644 index 0000000..e383777 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/repository/UserMetaRepository.java @@ -0,0 +1,14 @@ +package com.secure_use_api.repository; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.secure_use_api.model.UserMetaModel; + +@Repository +public interface UserMetaRepository extends JpaRepository { + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/repository/UserSecurityRepository.java b/securety_api/src/main/java/com/secure_use_api/repository/UserSecurityRepository.java new file mode 100644 index 0000000..5eda66c --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/repository/UserSecurityRepository.java @@ -0,0 +1,12 @@ +package com.secure_use_api.repository; + +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.secure_use_api.model.UserSecurityModel; + +@Repository +public interface UserSecurityRepository extends JpaRepository { +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/controller/ApiTokenController.java b/securety_api/src/main/java/com/secure_use_api/rest/controller/ApiTokenController.java new file mode 100644 index 0000000..f0d108b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/controller/ApiTokenController.java @@ -0,0 +1,73 @@ +package com.secure_use_api.rest.controller; + +import java.util.List; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import org.springframework.security.core.context.SecurityContextHolder; + +import com.secure_use_api.exceptions.Exceptions.UserAlreadyExists; +import com.secure_use_api.rest.dto.TokensDto; +import com.secure_use_api.rest.dto.IdActionDto; +import com.secure_use_api.rest.service.ApiTokenService; +import com.secure_use_api.security.RequireScope; +import com.secure_use_api.security.UserAuthentication; +import com.secure_use_api.token.ApiToken; + +@RestController +@RequestMapping("api") +public class ApiTokenController { + + @Autowired + private ApiTokenService service; + + @GetMapping(path = "getAll") + @RequireScope({ "authenticated", "token:read" }) + public ResponseEntity> getAll() { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + + try { + List apiTokenList = this.service.getAllAsStrings(user.getUserId()); + + return ResponseEntity.ok(apiTokenList); + } catch (UserAlreadyExists err) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, err.getMessage()); + } + } + + @PostMapping(path = "create", produces = MediaType.APPLICATION_JSON_VALUE) + @RequireScope({ "authenticated", "token:create" }) + public ResponseEntity create() { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + + try { + ApiToken apiToken = this.service.create(user.getUserId(), user.getName()); + + return ResponseEntity.ok(new TokensDto(apiToken.toTokenString(), null)); + } catch (UserAlreadyExists err) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, err.getMessage()); + } + } + + @DeleteMapping(path = "delete/{apiTokenId}") + @RequireScope({ "authenticated", "token:delete" }) + public ResponseEntity delete(@PathVariable UUID apiTokenId) { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + + int deleteCount = this.service.delete(apiTokenId, user.getUserId()); + + if (deleteCount != 0) { + return ResponseEntity.ok(new IdActionDto(apiTokenId, "TokenDeleted")); + } + + // This can be happen when there is not token with the given id or it is not + // owned by the user + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No Token deleted"); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/controller/AuthenticationController.java b/securety_api/src/main/java/com/secure_use_api/rest/controller/AuthenticationController.java new file mode 100644 index 0000000..120161a --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/controller/AuthenticationController.java @@ -0,0 +1,132 @@ +package com.secure_use_api.rest.controller; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import com.auth0.jwt.exceptions.JWTVerificationException; + +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.secure_use_api.exceptions.Exceptions.ExpiredException; +import com.secure_use_api.exceptions.Exceptions.InvalidException; +import com.secure_use_api.exceptions.Exceptions.UserAlreadyExists; +import com.secure_use_api.rest.dto.TokensDto; +import com.secure_use_api.rest.dto.IdActionDto; +import com.secure_use_api.rest.dto.RefreshTokenDto; +import com.secure_use_api.rest.dto.UserLoginDto; +import com.secure_use_api.rest.dto.UserRegistrationDto; +import com.secure_use_api.rest.service.RefreshTokenService; +import com.secure_use_api.rest.service.UserService; +import com.secure_use_api.rest.service.AuthenticationService; +import com.secure_use_api.security.RequireScope; +import com.secure_use_api.security.Role; +import com.secure_use_api.security.UserAuthentication; +import com.secure_use_api.token.AccessToken; +import com.secure_use_api.token.RefreshToken; + +@RestController +@RequestMapping("auth") +public class AuthenticationController { + + @Autowired + private AuthenticationService authService; + + @Autowired + private UserService userService; + + @Autowired + private RefreshTokenService refreshTokenService; + + @GetMapping({ "", "/" }) + @RequireScope({ "authenticated", "account:read" }) + String home() { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + List roles = user.getAuthorities().stream() + .filter(authority -> authority instanceof Role) // Filter to ensure it's a Role + .map(authority -> (Role) authority) // Cast to Role + .collect(Collectors.toList()); // Collect to List + + return "Hello World! with " + roles.toString(); + } + + @Transactional + @PostMapping(path = "register", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity register(@RequestBody @Valid UserRegistrationDto registrationDto) { + + try { + UUID userId = this.authService.register(registrationDto.getEmail(), registrationDto.getPassword()); + + this.userService.createUserAdditions(userId); + + return ResponseEntity.ok(new IdActionDto(userId, "UserCreated")); + } catch (UserAlreadyExists err) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, err.getMessage()); + } + } + + @PostMapping(path = "login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity login(@RequestBody @Valid UserLoginDto loginRequest) { + try { + AccessToken accessToken = this.authService.login(loginRequest.getEmail(), loginRequest.getPassword()); + RefreshToken refreshToken = refreshTokenService.create(accessToken); + + System.out.println(accessToken); + System.out.println("lbub"); + + return ResponseEntity.ok(new TokensDto(accessToken.toTokenString(), refreshToken.toTokenString())); + } catch (AuthenticationException err) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, err.getMessage()); + } + } + + @PutMapping(path = "login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity refresh( + @RequestHeader(value = "Authorization", required = false) String authorizationHeader, + @RequestBody @Valid RefreshTokenDto refreshTokenRequest) { + try { + if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing Authorization Bearer Token"); + } + + Optional accessToken = AccessToken.fromTokenString(authorizationHeader.substring(7)); + Optional refreshToken = RefreshToken.fromTokenString(refreshTokenRequest.getRefreshToken()); + + if (!accessToken.isPresent()) { + throw new InvalidException("Authorization Header does not contain a Valid AccessToken"); + } else if (!refreshToken.isPresent()) { + throw new InvalidException("RefreshToken is not a Valid Token"); + } + + AccessToken newAccessToken = refreshTokenService.devaluate(refreshToken.get(), accessToken.get()); + RefreshToken newRefreshToken = refreshTokenService.create(newAccessToken); + + return ResponseEntity.ok(new TokensDto(newAccessToken.toTokenString(), newRefreshToken.toTokenString())); + } catch (JWTVerificationException | ExpiredException | InvalidException | IllegalArgumentException + | AuthenticationException err) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, err.getMessage()); + } + } + + @DeleteMapping(path = "delete") + @RequireScope({ "authenticated", "account:delete" }) + public ResponseEntity delete() { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + + this.authService.delete(user.getUserId()); + + return ResponseEntity.ok(new IdActionDto(user.getUserId(), "UserDeleted")); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/controller/UserController.java b/securety_api/src/main/java/com/secure_use_api/rest/controller/UserController.java new file mode 100644 index 0000000..e400569 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/controller/UserController.java @@ -0,0 +1,48 @@ +package com.secure_use_api.rest.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import jakarta.transaction.Transactional; + +import org.springframework.security.core.context.SecurityContextHolder; + +import com.secure_use_api.rest.dto.ResourceLimitLeftDto; +import com.secure_use_api.rest.middleware.ResourceLimitMiddleware.ResourceLimited; +import com.secure_use_api.rest.service.UserService; +import com.secure_use_api.security.RequireScope; +import com.secure_use_api.security.UserAuthentication; + +@RestController +@RequestMapping("user") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping(path = "longRun") + @ResourceLimited + public ResponseEntity longRunningTask() { + try { + // Simulate a long-running task (e.g., 20 seconds) + Thread.sleep(20000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + // Handle the interruption + } + + return ResponseEntity.ok("Finished"); + } + + @Transactional + @GetMapping(path = "resourceLimit") + @RequireScope({ "authenticated", "account:read" }) + public ResponseEntity getResourceLimitLeft() { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext().getAuthentication(); + + long resourceLimit = userService.getResourceLimitLeft(user.getUserId()); + + return ResponseEntity.ok(new ResourceLimitLeftDto(resourceLimit, "seconds")); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/IdActionDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/IdActionDto.java new file mode 100644 index 0000000..09148f4 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/IdActionDto.java @@ -0,0 +1,28 @@ +package com.secure_use_api.rest.dto; + +import java.util.UUID; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class IdActionDto { + + @Size(min = 1, max = 32) + private String actionType; + + @NotNull + private UUID id; + + public IdActionDto(UUID id, String actionType) { + this.id = id; + this.actionType = actionType; + } + + public UUID getId() { + return this.id; + } + + public String getActionType() { + return this.actionType; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/RefreshTokenDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/RefreshTokenDto.java new file mode 100644 index 0000000..8558de8 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/RefreshTokenDto.java @@ -0,0 +1,21 @@ +package com.secure_use_api.rest.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class RefreshTokenDto { + + @NotNull(message = "RefreshToken cannot be null") + @NotEmpty(message = "RefreshToken cannot be empty") + @Size(min = 8, max = 4096) + private String refreshToken; + + public RefreshTokenDto() { + this.refreshToken = ""; + } + + public String getRefreshToken() { + return this.refreshToken; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/ResourceLimitLeftDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/ResourceLimitLeftDto.java new file mode 100644 index 0000000..adee85b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/ResourceLimitLeftDto.java @@ -0,0 +1,30 @@ +package com.secure_use_api.rest.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class ResourceLimitLeftDto { + + @NotNull + @Min(-999999999) + @Max(999999999) + private final long time; + + @Size(min = 1, max = 32) + private final String unitType; + + public ResourceLimitLeftDto(long time, String unitType) { + this.time = time; + this.unitType = unitType; + } + + public long getTime() { + return this.time; + } + + public String getUnitType() { + return this.unitType; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/TokensDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/TokensDto.java new file mode 100644 index 0000000..97988c8 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/TokensDto.java @@ -0,0 +1,26 @@ +package com.secure_use_api.rest.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class TokensDto { + @NotNull + @Size(min = 8, max = 4096) + private final String token; + + @Size(min = 8, max = 4096) + private final String refreshToken; + + public TokensDto(String token, String refreshToken) { + this.token = token; + this.refreshToken = refreshToken; + } + + public String getToken() { + return this.token; + } + + public String getRefreshToken() { + return this.refreshToken; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/UserLoginDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/UserLoginDto.java new file mode 100644 index 0000000..c35871b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/UserLoginDto.java @@ -0,0 +1,33 @@ +package com.secure_use_api.rest.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class UserLoginDto { + + @NotNull(message = "Email cannot be null") + @NotEmpty(message = "Email cannot be empty") + @Size(min = 4, max = 56) + @Email + private String email; + + @NotNull(message = "Password cannot be null") + @NotEmpty(message = "Password cannot be empty") + @Size(min = 8, max = 56) + private String password; + + public UserLoginDto() { + this.email = ""; + this.password = ""; + } + + public String getEmail() { + return this.email; + } + + public String getPassword() { + return this.password; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/dto/UserRegistrationDto.java b/securety_api/src/main/java/com/secure_use_api/rest/dto/UserRegistrationDto.java new file mode 100644 index 0000000..70e9190 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/dto/UserRegistrationDto.java @@ -0,0 +1,28 @@ +package com.secure_use_api.rest.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class UserRegistrationDto { + + @NotNull(message = "Email cannot be null") + @NotEmpty(message = "Email cannot be empty") + @Size(min = 4, max = 56) + @Email + private String email; + + @NotNull(message = "Password cannot be null") + @NotEmpty(message = "Password cannot be empty") + @Size(min = 8, max = 56) + private String password; + + public String getEmail() { + return this.email; + } + + public String getPassword() { + return this.password; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuditLogMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuditLogMiddleware.java new file mode 100644 index 0000000..4350f43 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuditLogMiddleware.java @@ -0,0 +1,83 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.security.UserAuthentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component +public class AuditLogMiddleware extends OncePerRequestFilter { + private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z"); + + private final Logger logger = LoggerFactory.getLogger(AuditLogMiddleware.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + System.out.println("AuditLogMiddleware start"); + + // Needing cachedResponse for getting Body which is a one time readable stream. + // With not doing it that way but reading the body here, result in an empty body + // for the client. + ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(response); + + String requestTime = OffsetDateTime.now().format(dateTimeFormatter); + String host = request.getRemoteAddr(); + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + String protocol = request.getProtocol(); + String referer = request.getHeader("Referer"); + String userAgent = request.getHeader("User-Agent"); + + // Continue the filter chain + filterChain.doFilter(request, cachedResponse); + + Authentication anonymousUser = SecurityContextHolder.getContext().getAuthentication(); + UserAuthentication user = null; + + System.out.println(anonymousUser); + + // If user is know and of instance UserAuthentication and not + // org.springframework.security.authentication.AnonymousAuthenticationToken + if (anonymousUser != null && anonymousUser instanceof UserAuthentication) { + user = (UserAuthentication) anonymousUser; + } + + // Log the response details + int status = cachedResponse.getStatus(); + byte[] responseBody = cachedResponse.getContentAsByteArray(); + + String logEntry = String.format("%s - %s [%s] \"%s %s %s\" %d %d %s %s", + host, + user != null ? user.getUserId() : "-", + requestTime, + method, + requestURI, + protocol, + status, + responseBody.length, + referer != null ? "\"" + referer + "\"" : "-", + userAgent != null ? "\"" + userAgent + "\"" : "-"); + + logger.info(logEntry); + + cachedResponse.copyBodyToResponse(); + + System.out.println("AuditLogMiddleware end"); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthenticationMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthenticationMiddleware.java new file mode 100644 index 0000000..1fb2668 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthenticationMiddleware.java @@ -0,0 +1,83 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.exceptions.ExceptionResponse; +import com.secure_use_api.exceptions.Exceptions.ExpiredException; +import com.secure_use_api.exceptions.Exceptions.InvalidException; +import com.secure_use_api.rest.service.ApiTokenService; +import com.secure_use_api.security.UserAuthentication; +import com.secure_use_api.token.AccessToken; +import com.secure_use_api.token.ApiToken; + +@Component +public class AuthenticationMiddleware extends OncePerRequestFilter { + @Autowired + private ApiTokenService service; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + System.out.println("AuthenticationMiddleware start"); + + String authorizationHeader = request.getHeader("Authorization"); + + System.out.println("hey"); + System.out.println(authorizationHeader); + + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + try { + // Extract the token by removing the "Bearer " prefix + String jwtString = authorizationHeader.substring(7); + Optional accessTokenOptional = ApiToken.fromTokenString(jwtString); + AccessToken accessToken; + Optional apiToken; + + if (!accessTokenOptional.isPresent()) { + throw new InvalidException("Authorization Header does not contain a Valid AccessToken"); + } + + accessToken = accessTokenOptional.get(); + apiToken = ApiToken.fromAccessToken(accessToken); + + // If the accessToken is an apiToken (accessToken without expireTime) + if (apiToken.isPresent()) { + // Since ApiTokens don't have an expire time, we check presents in db to make + // them manually revokable by removing + if (this.service.get(accessToken.getUid(), accessToken.getUserId()).isEmpty()) { + throw new ExpiredException("Token has expire"); + } + } + + UserAuthentication user = new UserAuthentication(accessToken.getUserId(), accessToken.getEmail(), + accessToken.getRoles()); + + SecurityContextHolder.getContext().setAuthentication(user); + } catch (InvalidException | ExpiredException e) { + ExceptionResponse exRes = new ExceptionResponse(HttpStatus.UNAUTHORIZED, "Authentication Token wrong", + request.getRequestURI()); + + exRes.writeToResponse(response, request); + + return; + } + } + + filterChain.doFilter(request, response); + + System.out.println("AuthenticationMiddleware end"); + } + +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthorizationMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthorizationMiddleware.java new file mode 100644 index 0000000..8dd048e --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/AuthorizationMiddleware.java @@ -0,0 +1,73 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; +import java.lang.reflect.Method; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.exceptions.ExceptionResponse; +import com.secure_use_api.security.RequireScope; +import com.secure_use_api.security.Role; +import com.secure_use_api.security.UserAuthentication; + +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AuthorizationMiddleware implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException, IOException { + + System.out.println("AuthorizationMiddleware start"); + + // Checking for handler to get the target controller method and associated + // Annotations (Reason why this is an implementation from HandlerInterceptor and + // not Filter) + + if (handler != null && handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + RequireScope requireScope = method.getAnnotation(RequireScope.class); + + if (requireScope != null) { + String[] requiredScopes = requireScope.value(); + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext() + .getAuthentication(); + + // If no user is present, but the method has at leas an empty @RequireScope, + // access is permitted + if (user == null) { + ExceptionResponse exRes = new ExceptionResponse(HttpStatus.UNAUTHORIZED, + HttpStatus.UNAUTHORIZED.getReasonPhrase(), + request.getRequestURI()); + + exRes.writeToResponse(response, request); + + return false; + } + + // Check if the user has the required Scopes + if (!Role.checkScope(user.getRoles(), requiredScopes)) { + ExceptionResponse exRes = new ExceptionResponse(HttpStatus.FORBIDDEN, + HttpStatus.FORBIDDEN.getReasonPhrase(), + request.getRequestURI()); + + exRes.writeToResponse(response, request); + + return false; + } + } + } + + System.out.println("AuthorizationMiddleware end"); + + return true; // Continue the request + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/ExceptionHandlerMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/ExceptionHandlerMiddleware.java new file mode 100644 index 0000000..dc1dee6 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/ExceptionHandlerMiddleware.java @@ -0,0 +1,26 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.exceptions.ExceptionResponse; + +// Allows Filters to throw the custom ExceptionResponse which is written to the response and is made to be read by the client +@Component +public class ExceptionHandlerMiddleware extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (ExceptionResponse e) { + e.writeToResponse(response, request); + } + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/RateLimitMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/RateLimitMiddleware.java new file mode 100644 index 0000000..5c824cc --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/RateLimitMiddleware.java @@ -0,0 +1,45 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.google.common.util.concurrent.RateLimiter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.exceptions.ExceptionResponse; +import com.secure_use_api.util.Config; + +@Component +public class RateLimitMiddleware extends OncePerRequestFilter { + + // Create a rate limiter allowing 1000 requests per second + private RateLimiter rateLimiter = RateLimiter.create(Config.getInstance().getRateLimitPerSecond()); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + System.out.println("RateLimitMiddleware start"); + + // Acquire a permit or return 429 Too Many Requests + if (!rateLimiter.tryAcquire()) { + ExceptionResponse exRes = new ExceptionResponse(HttpStatus.TOO_MANY_REQUESTS, new String("Try again later"), + request.getRequestURI()); + + exRes.writeToResponse(response, request); + + return; + } + + filterChain.doFilter(request, response); + + System.out.println("RateLimitMiddleware end"); + } + +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/middleware/ResourceLimitMiddleware.java b/securety_api/src/main/java/com/secure_use_api/rest/middleware/ResourceLimitMiddleware.java new file mode 100644 index 0000000..d955a65 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/middleware/ResourceLimitMiddleware.java @@ -0,0 +1,92 @@ +package com.secure_use_api.rest.middleware; + +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.time.Instant; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import com.secure_use_api.exceptions.ExceptionResponse; +import com.secure_use_api.rest.service.UserService; +import com.secure_use_api.security.UserAuthentication; + +@Component +public class ResourceLimitMiddleware implements HandlerInterceptor { + + // Is used to resource limit controller methods with this annotation + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface ResourceLimited {} + + @Autowired + private UserService userService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws ServletException, IOException { + + System.out.println("ResourceLimitMiddleware start"); + + if (handler != null && handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + Method method = handlerMethod.getMethod(); + ResourceLimited performResourceLimit = method.getAnnotation(ResourceLimited.class); + + if (performResourceLimit != null) { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext() + .getAuthentication(); + + long resourceLimitLeft = userService.getResourceLimitLeft(user.getUserId()); + + if (resourceLimitLeft <= 0) { + ExceptionResponse exRes = new ExceptionResponse(HttpStatus.FORBIDDEN, + "Daily ResourceLimit reached", + request.getRequestURI()); + + exRes.writeToResponse(response, request); + + return false; + } + + long preTimestampInSeconds = Instant.now().getEpochSecond(); + + request.setAttribute("ResourceLimitMiddleware__resourceLimitLeft", resourceLimitLeft); + request.setAttribute("ResourceLimitMiddleware__preTimestampInSeconds", preTimestampInSeconds); + } + } + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + UserAuthentication user = (UserAuthentication) SecurityContextHolder.getContext() + .getAuthentication(); + Long resourceLimitLeft = (Long) request.getAttribute("ResourceLimitMiddleware__resourceLimitLeft"); + Long preTimestampInSeconds = (Long) request.getAttribute("ResourceLimitMiddleware__preTimestampInSeconds"); + + if (resourceLimitLeft != null && preTimestampInSeconds != null) { + long postTimestampInSeconds = Instant.now().getEpochSecond(); + + userService.setResourceLimitLeft(user.getUserId(), + resourceLimitLeft - (postTimestampInSeconds - preTimestampInSeconds)); + } + + System.out.println("ResourceLimitMiddleware end"); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/service/ApiTokenService.java b/securety_api/src/main/java/com/secure_use_api/rest/service/ApiTokenService.java new file mode 100644 index 0000000..049745c --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/service/ApiTokenService.java @@ -0,0 +1,68 @@ +package com.secure_use_api.rest.service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import com.secure_use_api.model.ApiTokenModel; +import com.secure_use_api.repository.ApiTokenRepository; +import com.secure_use_api.security.Role; +import com.secure_use_api.token.ApiToken; + +@Service +public class ApiTokenService { + @Autowired + private ApiTokenRepository apiTokenRepo; + + @Transactional + public ApiToken create(UUID userId, String email) { + ArrayList roles = new ArrayList<>(); + ApiToken apiToken; + + roles.add(new Role(Role.Roles.API_TOKEN)); + + apiToken = new ApiToken(userId, email, roles); + + ApiTokenModel apiTokenModel = new ApiTokenModel(); + apiTokenModel.setUid(apiToken.getUid()); + apiTokenModel.setToken(apiToken.toTokenString()); + apiTokenModel.setOwner(userId); + + apiTokenRepo.save(apiTokenModel); + + return apiToken; + } + + @Transactional + public Optional get(UUID tokenId, UUID ownerId) { + ApiTokenModel apiTokenModel = this.apiTokenRepo.findByUidAndOwner(tokenId, ownerId); + + if (apiTokenModel != null) { + return ApiToken.fromString(apiTokenModel.getToken()); + } + + return Optional.empty(); + } + + @Transactional + public List getAllAsStrings(UUID userId) { + List apiTokenModelList = this.apiTokenRepo.findByOwner(userId); + List apiTokenList = new ArrayList<>(); + + for (ApiTokenModel apiTokenModel : apiTokenModelList) { + apiTokenList.add(apiTokenModel.getToken()); + } + + return apiTokenList; + } + + @Transactional + public int delete(UUID tokenId, UUID ownerId) { + return this.apiTokenRepo.deleteByUidAndOwner(tokenId, ownerId); + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/service/AuthenticationService.java b/securety_api/src/main/java/com/secure_use_api/rest/service/AuthenticationService.java new file mode 100644 index 0000000..ac646dd --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/service/AuthenticationService.java @@ -0,0 +1,87 @@ +package com.secure_use_api.rest.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Service; + +import de.mkammerer.argon2.Argon2; +import de.mkammerer.argon2.Argon2Factory; +import jakarta.transaction.Transactional; +import com.secure_use_api.exceptions.Exceptions.UserAlreadyExists; +import com.secure_use_api.model.UserMetaModel; +import com.secure_use_api.model.UserSecurityModel; +import com.secure_use_api.repository.UserMetaRepository; +import com.secure_use_api.repository.UserSecurityRepository; +import com.secure_use_api.security.Role; +import com.secure_use_api.security.UserDetailsImpl; +import com.secure_use_api.token.AccessToken; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +@Service +public class AuthenticationService { + @Autowired + private UserMetaRepository userMetaRepo; + + @Autowired + private UserSecurityRepository userSecurityRepo; + + @Autowired + private AuthenticationManager authManager; + + // * Must be the same as in the CustomAuthProvider.java + private final Argon2 passwordEncoder = Argon2Factory.create(); + + @Transactional + public UUID register(String username, String password) throws UserAlreadyExists { + + // Check if user already exists + if (userMetaRepo.findByEmail(username).isPresent()) { + throw new UserAlreadyExists("Username is already taken."); + } + + // Create new user + ArrayList roles = new ArrayList<>(); + UserMetaModel userMeta; + + roles.add(new Role(Role.Roles.USER).toString()); + + userMeta = new UserMetaModel(); + + userMeta.setEmail(username); + userMeta.setRoles(roles); + userMetaRepo.save(userMeta); + + UserSecurityModel userSecurity = new UserSecurityModel(); + String passwordHash = passwordEncoder.hash(22, 65536, 1, password.toCharArray()); + + userSecurity.setUserMeta(userMeta); + userSecurity.setPassword(passwordHash); + userSecurityRepo.save(userSecurity); + + return userMeta.getUserId(); + } + + @Transactional(dontRollbackOn = AuthenticationException.class) + public AccessToken login(String username, String password) throws AuthenticationException { + Authentication authentication = authManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password)); + + UserDetailsImpl user = (UserDetailsImpl) authentication.getPrincipal(); + + Collection roles = user.getAuthorities(); + + AccessToken accessToken = new AccessToken(user.getUserId(), user.getUsername(), new ArrayList<>(roles)); + + return accessToken; + } + + public void delete(UUID userId) { + userMetaRepo.deleteById(userId); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/rest/service/RefreshTokenService.java b/securety_api/src/main/java/com/secure_use_api/rest/service/RefreshTokenService.java new file mode 100644 index 0000000..e9745e1 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/service/RefreshTokenService.java @@ -0,0 +1,59 @@ +package com.secure_use_api.rest.service; + +import java.sql.Timestamp; +import java.util.Date; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import com.secure_use_api.exceptions.Exceptions.ExpiredException; +import com.secure_use_api.exceptions.Exceptions.InvalidException; +import com.secure_use_api.model.RefreshTokenModel; +import com.secure_use_api.repository.RefreshTokenRepository; +import com.secure_use_api.token.AccessToken; +import com.secure_use_api.token.RefreshToken; + +@Service +public class RefreshTokenService { + + @Autowired + private RefreshTokenRepository refreshTokenRepo; + + @Transactional + public RefreshToken create(AccessToken accessToken) { + RefreshToken refreshToken = new RefreshToken(accessToken.getUid()); + + RefreshTokenModel refreshTokenModel = new RefreshTokenModel(); + refreshTokenModel.setUid(refreshToken.getUid()); + refreshTokenModel.setSpouse(refreshToken.getSpouseId()); + refreshTokenModel.setExpireAt(new Timestamp(refreshToken.getExpiresAt().getTime())); + + refreshTokenRepo.save(refreshTokenModel); + + return refreshToken; + } + + @Transactional + public AccessToken devaluate(RefreshToken refreshToken, AccessToken spouseToken) + throws ExpiredException, InvalidException { + Date currentTime = new Date(); + + // Check if expiredAt Date is in the past + if (refreshToken.getExpiresAt().before(currentTime)) { + throw new ExpiredException("RefreshToken is expired"); + } + + int deletedCount = refreshTokenRepo.deleteExpiredToken(refreshToken.getUid(), spouseToken.getUid(), + currentTime); + + if (deletedCount == 0) { + throw new InvalidException("RefreshToken is not valid"); + } + + AccessToken accessToken = new AccessToken(spouseToken.getUserId(), spouseToken.getEmail(), + spouseToken.getRoles()); + + return accessToken; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/rest/service/UserService.java b/securety_api/src/main/java/com/secure_use_api/rest/service/UserService.java new file mode 100644 index 0000000..86fa310 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/rest/service/UserService.java @@ -0,0 +1,92 @@ +package com.secure_use_api.rest.service; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import com.secure_use_api.model.UserAdditionsModel; +import com.secure_use_api.model.UserMetaModel; +import com.secure_use_api.repository.UserAdditionsRepository; +import com.secure_use_api.repository.UserMetaRepository; +import com.secure_use_api.util.Config; + +@Service +public class UserService { + + public static int timeLimitPerDayInSeconds = Config.getInstance().getResourceLimitPerDay(); + + @Autowired + private UserMetaRepository userMetaRepo; + + @Autowired + private UserAdditionsRepository userAdditionsRepo; + + @Transactional + public void createUserAdditions(UUID userId) { + // This is a db call which is needed to get the userMeta. + // Other approaches are getting it as parameter which require returning + // the Entity by the other service function (exposing the Model) + // The other would be using a single Service doing all stuff + // which creates a tighter coupling causing less flexible and separation of + // concerns + UserMetaModel userMeta = userMetaRepo.findById(userId).get(); + + UserAdditionsModel userAdditionsModel = new UserAdditionsModel(); + userAdditionsModel.setUserMeta(userMeta); + userAdditionsModel.setResourceLimitUsed(0); + + userAdditionsRepo.save(userAdditionsModel); + } + + @Transactional + private long getResourceTimeUsed(UUID userId) { + // SELECT used WHERE uid = 1? AND lastUpdated = today; + // if used == 0 null = 0 + // Get today's date without time + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + Date todayStart = calendar.getTime(); + + calendar.set(Calendar.HOUR_OF_DAY, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + Date todayEnd = calendar.getTime(); + + UserAdditionsModel userAdditionsModel = userAdditionsRepo.findByUidAndToday(userId, + new Timestamp(todayStart.getTime()), new Timestamp(todayEnd.getTime())); + + // If not userAdditionsModel was found, it may hint that the last update was not + // done today, so the user has currently used 0 resources (or the user is not + // present in db) + if (userAdditionsModel == null) { + return 0; + } else { + return userAdditionsModel.getResourceLimitUsed(); + } + } + + public long getResourceLimitLeft(UUID userId) { + long resourceTimeUsed = getResourceTimeUsed(userId); + + return timeLimitPerDayInSeconds - resourceTimeUsed; + } + + @Transactional + public int setResourceLimitLeft(UUID userId, long resourceLimitLeft) { + // Recalculating the OverallResourceTimeUsed, depending on the resourceLimitLeft + long resourceTimeUsed = (resourceLimitLeft - timeLimitPerDayInSeconds) * -1; + int updateCount = userAdditionsRepo.UpdateResourceTimeUsedAndLastChangedAtByUid(userId, resourceTimeUsed, + Timestamp.from(Instant.now())); + + return updateCount; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/security/CustomAuthProvider.java b/securety_api/src/main/java/com/secure_use_api/security/CustomAuthProvider.java new file mode 100644 index 0000000..fb2e99c --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/CustomAuthProvider.java @@ -0,0 +1,71 @@ +package com.secure_use_api.security; + +import java.sql.Timestamp; +import java.util.Date; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import de.mkammerer.argon2.Argon2; +import de.mkammerer.argon2.Argon2Factory; +import com.secure_use_api.model.UserComposite; +import com.secure_use_api.model.UserMetaModel; +import com.secure_use_api.model.UserSecurityModel; +import com.secure_use_api.repository.UserMetaRepository; +import com.secure_use_api.repository.UserSecurityRepository; + +@Component +public class CustomAuthProvider implements AuthenticationProvider { + + @Autowired + private UserMetaRepository userMetaRepository; + + @Autowired + private UserSecurityRepository userSecurityRepository; + + private final Argon2 passwordEncoder = Argon2Factory.create(); + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String email = authentication.getName(); + String password = authentication.getCredentials().toString(); + + UserComposite user = loadUserByUsername(email); + UserDetailsImpl userDetails = UserDetailsImpl.build(user); + + if (!passwordEncoder.verify(userDetails.getPassword(), password.toCharArray())) { + int failedLogInAttempts = user.getUserSecurity().getFailedLogInAttempts() + 1; + + user.getUserSecurity().setFailedLogInAttempts(failedLogInAttempts); + this.userSecurityRepository.save(user.getUserSecurity()); + + throw new BadCredentialsException("Invalid password"); + } + + user.getUserSecurity().setFailedLogInAttempts(0); + user.getUserSecurity().setLastLogInAt(new Timestamp(new Date().getTime())); + this.userSecurityRepository.save(user.getUserSecurity()); + + return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } + + private UserComposite loadUserByUsername(String email) throws UsernameNotFoundException { + UserMetaModel userMeta = userMetaRepository.findByEmail(email) // + .orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email)); + + UserSecurityModel userSecurity = userSecurityRepository.findById(userMeta.getUserId()).get(); + + return new UserComposite(userMeta, userSecurity); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/security/RequireScope.java b/securety_api/src/main/java/com/secure_use_api/security/RequireScope.java new file mode 100644 index 0000000..3569b7c --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/RequireScope.java @@ -0,0 +1,12 @@ +package com.secure_use_api.security; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface RequireScope { + String[] value(); // Define the scopes required for the method +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/security/Role.java b/securety_api/src/main/java/com/secure_use_api/security/Role.java new file mode 100644 index 0000000..000ac4b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/Role.java @@ -0,0 +1,73 @@ +package com.secure_use_api.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; + +public class Role implements GrantedAuthority { + + public static enum Roles { + API_TOKEN, + USER, + } + + private static final Map> roleScopesMap = new HashMap<>(); + + static { + // Initialize the map with roles and their corresponding scopes + roleScopesMap.put(Roles.USER, List.of("authenticated", "account:read", "account:delete", "token:create", + "token:read", "token:delete")); + roleScopesMap.put(Roles.API_TOKEN, List.of("authenticated", "account:read", "token:read")); + } + + private final Roles role; + + private Collection scopes; + + public static boolean checkScope(Collection roles, String[] requiredScopes) { + List requiredScopesList = new ArrayList<>(Arrays.asList(requiredScopes)); + + for (Role role : roles) { + if (requiredScopesList.isEmpty()) { + break; + } + + requiredScopesList.removeAll(role.getScopes()); + } + + // If all requiredScopes where removed by being present in roles, the check is + // successful + return requiredScopesList.isEmpty(); + } + + public Role(Roles role) { + this.role = role; + + Collection scopes = roleScopesMap.get(role); + + this.scopes = scopes != null ? scopes : List.of(); + } + + @Override + public String getAuthority() { + return this.role.toString(); + } + + public Collection getScopes() { + return this.scopes; + } + + @Override + public String toString() { + return this.role.toString(); + } + + public static Role fromString(String roleString) { + return new Role(Roles.valueOf(roleString)); + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/security/SecurityConfig.java b/securety_api/src/main/java/com/secure_use_api/security/SecurityConfig.java new file mode 100644 index 0000000..d3c1f8b --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/SecurityConfig.java @@ -0,0 +1,72 @@ +package com.secure_use_api.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.web.SecurityFilterChain; +import org.springframework.security.web.context.SecurityContextHolderFilter; +import org.springframework.security.web.session.DisableEncodeUrlFilter; + +import com.secure_use_api.exceptions.CustomAuthenticationEntryPoint; +import com.secure_use_api.rest.middleware.AuditLogMiddleware; +import com.secure_use_api.rest.middleware.AuthenticationMiddleware; +import com.secure_use_api.rest.middleware.ExceptionHandlerMiddleware; +import com.secure_use_api.rest.middleware.RateLimitMiddleware; + +@Configuration +@EnableWebSecurity(debug = true) +public class SecurityConfig { + @Autowired + private ExceptionHandlerMiddleware exceptionHandlerMiddleware; + + @Autowired + private RateLimitMiddleware rateLimitMiddleware; + + @Autowired + private AuthenticationMiddleware authenticationMiddleware; + + @Autowired + private AuditLogMiddleware auditLogMiddleware; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) + throws Exception { + // Since CustomAuthProvider is annotated with @Component, spring detects it as + // Bean and use it for dependency injection in the automatically created default + // AuthenticationManager because it is implementing the AuthenticationProvider + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) // disable crsf because we are stateless (no cookies for authentication) + .authorizeHttpRequests((request) -> request + .requestMatchers(HttpMethod.GET, "/h2-console/**").permitAll() + .requestMatchers(HttpMethod.POST, "/auth/register", "/auth/login", "/h2-console/**").permitAll() + .requestMatchers(HttpMethod.PUT, "/auth/login").permitAll() + .requestMatchers("/error").permitAll() + .anyRequest().authenticated()) // Require all request except the above to be authenticated + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(exceptionHandlerMiddleware, DisableEncodeUrlFilter.class) // The very first filter + .addFilterAfter(rateLimitMiddleware, ExceptionHandlerMiddleware.class) + .addFilterAfter(auditLogMiddleware, SecurityContextHolderFilter.class) + .addFilterAfter(authenticationMiddleware, AuditLogMiddleware.class) + .exceptionHandling((exception) -> exception + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())); + + // Need to set the AuditLog before AuthenticationMiddleware (and after + // SecurityContextHolderFilter to access it), because we also + // want to log errors happening in the AuthenticationMiddleware which would not + // be possible if being executed after it. We can still get the user if it + // exists after the request was processed (so getting it on the way out and not + // in) + + return http.build(); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/security/UserAuthentication.java b/securety_api/src/main/java/com/secure_use_api/security/UserAuthentication.java new file mode 100644 index 0000000..0cfe793 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/UserAuthentication.java @@ -0,0 +1,76 @@ +package com.secure_use_api.security; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.UUID; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +public class UserAuthentication implements Authentication { + + private final UUID userId; // Custom field for user ID + private final String email; // Username + private Object credentials; + private Object details; + private final Collection roles; // User roles + private boolean authenticated; // Authentication status + + public UserAuthentication(UUID userId, String email, Collection authorities) { + this.userId = userId; + this.email = email; + this.roles = authorities != null ? authorities : new ArrayList<>(); + this.authenticated = true; // Set to true upon successful authentication + } + + @Override + public String getName() { + return this.email; + } + + @Override + public Collection getAuthorities() { + return this.roles; + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getDetails() { + return this.details; + } + + @Override + public Object getPrincipal() { + return this.email; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) { + this.authenticated = isAuthenticated; + } + + public UUID getUserId() { + return this.userId; + } + + public Collection getRoles() { + return this.roles; + } + + public void setCredentials(Object credentials) { + this.credentials = credentials; + } + + public void setDetails(Object details) { + this.details = details; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/security/UserDetailsImpl.java b/securety_api/src/main/java/com/secure_use_api/security/UserDetailsImpl.java new file mode 100644 index 0000000..0b67f41 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/UserDetailsImpl.java @@ -0,0 +1,64 @@ +package com.secure_use_api.security; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.security.core.userdetails.UserDetails; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import com.secure_use_api.model.UserComposite; + +public class UserDetailsImpl implements UserDetails { + + private final UUID userId; // Custom field for user ID + + private final String email; // Email + + @JsonIgnore + private String password; + + private final Collection authorities; // User roles + + public UserDetailsImpl(UUID userId, String email, String password, + Collection authorities) { + this.userId = userId; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(UserComposite user) { + + List roles = user.getUserMeta().getRoles().stream() + .map(Role::fromString) + .collect(Collectors.toList()); + + return new UserDetailsImpl( + user.getUserMeta().getUserId(), + user.getUserMeta().getEmail(), + user.getUserSecurity().getPassword(), + roles); + } + + public UUID getUserId() { + return this.userId; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.email; + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/security/WebConfig.java b/securety_api/src/main/java/com/secure_use_api/security/WebConfig.java new file mode 100644 index 0000000..74cd505 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/security/WebConfig.java @@ -0,0 +1,43 @@ +package com.secure_use_api.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.secure_use_api.rest.middleware.AuthorizationMiddleware; +import com.secure_use_api.rest.middleware.ResourceLimitMiddleware; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Autowired + private AuthorizationMiddleware authorizationMiddleware; + + @Autowired + private ResourceLimitMiddleware resourceLimitMiddleware; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authorizationMiddleware); + + registry.addInterceptor(resourceLimitMiddleware) + .addPathPatterns("/user/**"); + + // Register the ResourceLimitMiddleware + // registry.addInterceptor(resourceLimitMiddleware) + // .addPathPatterns("/api/**") // Specify URL patterns for resource limit + // .order(1); // Ensure it runs after AuthorizationMiddleware + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + // Simply setting Access-Control Headers + registry.addMapping("/**") + // .allowedOrigins("https://example.com") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("Content-Type", "Authorization") + .allowCredentials(false); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/token/AbstractToken.java b/securety_api/src/main/java/com/secure_use_api/token/AbstractToken.java new file mode 100644 index 0000000..907d6e0 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/AbstractToken.java @@ -0,0 +1,12 @@ +package com.secure_use_api.token; + +import java.lang.reflect.Type; +import java.util.Map; + +import com.google.gson.reflect.TypeToken; + +public abstract class AbstractToken { + protected static Type payloadType = new TypeToken>() {}.getType(); + + protected static SimpleTokenStringHandler tokenStringHandler; +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/AccessToken.java b/securety_api/src/main/java/com/secure_use_api/token/AccessToken.java new file mode 100644 index 0000000..13ad6b8 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/AccessToken.java @@ -0,0 +1,133 @@ +package com.secure_use_api.token; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.secure_use_api.security.Role; +import com.secure_use_api.util.Config; + +public class AccessToken extends AbstractToken { + static long expirationTime = 720000; + protected static SimpleTokenStringHandler tokenStringHandler = new JwtHandler( + Config.getInstance().getSecretAccessTokenKey()); + + protected final UUID uid; + protected final UUID userId; + protected final String email; + protected final List roles; + protected final Optional expiresAt; + + public class InnerAccessToken { + public String jti; + public String sub; + public String userId; + public Long exp; + public List roles; + } + + public AccessToken(UUID userId, String email, List roles) { + this.uid = UUID.randomUUID(); + this.userId = userId; + this.email = email; + this.roles = roles; + this.expiresAt = Optional.of(new Date(System.currentTimeMillis() + expirationTime)); + } + + protected AccessToken(UUID uid, UUID userId, String email, List roles, Optional expiresAt) { + this.uid = uid; + this.userId = userId; + this.email = email; + this.roles = roles; + this.expiresAt = expiresAt; + } + + static public Optional fromTokenString(String tokeString) throws IllegalArgumentException { + Optional payloadString = tokenStringHandler.payloadFromTokenString(tokeString); + + if (payloadString.isPresent()) { + Gson json = new Gson(); + InnerAccessToken payload = json.fromJson(payloadString.get(), InnerAccessToken.class); + + String uidString = payload.jti; + String email = payload.sub; + Long expiresAtLong = payload.exp; + String userIdString = payload.userId; + List rolesAStrings = payload.roles; + List roles; + + if (uidString == null || uidString.isEmpty()) { + throw new IllegalArgumentException("Missing uid"); + } + + if (userIdString == null || userIdString.isEmpty()) { + throw new IllegalArgumentException("Missing userId"); + } + + if (email == null || email.isEmpty()) { + throw new IllegalArgumentException("Missing email"); + } + + if (rolesAStrings == null) { + throw new IllegalArgumentException("No roles present"); + } else { + roles = rolesAStrings.stream() + .map(Role::fromString) + .collect(Collectors.toList()); + } + + UUID uid = UUID.fromString(uidString); + UUID userId = UUID.fromString(userIdString); + Optional expiresAtDate = expiresAtLong != null ? Optional.of(new Date(expiresAtLong * 1000)) + : Optional.empty(); + + return Optional.of(new AccessToken(uid, userId, email, roles, expiresAtDate)); + } + + return Optional.empty(); + } + + public String toTokenString() { + Map payload = new HashMap<>(); + List rolesAsStrings = this.roles.stream() + .map(Role::toString) + .collect(Collectors.toList()); + + payload.put("jti", this.uid.toString()); + payload.put("sub", this.email); + payload.put("iat", new Date().getTime() / 1000); + payload.put("userId", this.userId.toString()); + payload.put("roles", rolesAsStrings); + + if (this.expiresAt.isPresent()) { + payload.put("exp", this.expiresAt.get().getTime() / 1000); + } + + return tokenStringHandler.payloadToTokenString(payload); + } + + public UUID getUid() { + return this.uid; + } + + public UUID getUserId() { + return this.userId; + } + + public String getEmail() { + return this.email; + } + + public List getRoles() { + return this.roles; + } + + public boolean isLongLife() { + return !this.expiresAt.isPresent(); + } +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/ApiToken.java b/securety_api/src/main/java/com/secure_use_api/token/ApiToken.java new file mode 100644 index 0000000..0240120 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/ApiToken.java @@ -0,0 +1,37 @@ +package com.secure_use_api.token; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import com.secure_use_api.security.Role; + +public class ApiToken extends AccessToken { + public static Optional fromAccessToken(AccessToken accessToken) { + if (accessToken.isLongLife()) { + return Optional.of(new ApiToken(accessToken.getUid(), accessToken.getUserId(), accessToken.getEmail(), + accessToken.getRoles())); + } + + return Optional.empty(); + } + + public static Optional fromString(String tokenString) { + Optional accessTokenOptional = AccessToken.fromTokenString(tokenString); + + if (accessTokenOptional.isPresent()) { + return ApiToken.fromAccessToken(accessTokenOptional.get()); + } + + return Optional.empty(); + } + + public ApiToken(UUID userId, String email, List roles) { + super(UUID.randomUUID(), userId, email, roles, Optional.empty()); + } + + private ApiToken(UUID uid, UUID userId, String email, List roles) { + super(uid, userId, email, roles, Optional.empty()); + } + +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/JwtHandler.java b/securety_api/src/main/java/com/secure_use_api/token/JwtHandler.java new file mode 100644 index 0000000..b78a2c0 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/JwtHandler.java @@ -0,0 +1,47 @@ +package com.secure_use_api.token; + +import java.lang.reflect.Type; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.gson.reflect.TypeToken; + +public class JwtHandler implements SimpleTokenStringHandler { + public static Type payloadType = new TypeToken>() {}.getType(); + + private Algorithm algorithm; + private JWTVerifier verifier; + + public JwtHandler(String secretKey) { + this.algorithm = Algorithm.HMAC256(secretKey); + this.verifier = JWT.require(algorithm) + // .withIssuer("HAW-Hamburg") + .build(); + } + + @Override + public Optional payloadFromTokenString(String tokeString) { + + try { + DecodedJWT jwt = this.verifier.verify(tokeString); + byte[] decodedBytes = Base64.getDecoder().decode(jwt.getPayload()); + String decodedString = new String(decodedBytes); + + return Optional.of(decodedString); + } catch(Exception _e) { + System.out.println(_e); + return Optional.empty(); + } + } + + @Override + public String payloadToTokenString(Map payload) { + return JWT.create().withPayload(payload).sign(this.algorithm); + } + +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/PasetoHandler.java b/securety_api/src/main/java/com/secure_use_api/token/PasetoHandler.java new file mode 100644 index 0000000..6de87ea --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/PasetoHandler.java @@ -0,0 +1,19 @@ +package com.secure_use_api.token; + +import java.util.Map; +import java.util.Optional; + +public class PasetoHandler implements SimpleTokenStringHandler { + @Override + public Optional payloadFromTokenString(String tokeString) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'payloadFromTokenString'"); + } + + @Override + public String payloadToTokenString(Map payload) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'payloadToTokenString'"); + } + +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/RefreshToken.java b/securety_api/src/main/java/com/secure_use_api/token/RefreshToken.java new file mode 100644 index 0000000..480b77f --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/RefreshToken.java @@ -0,0 +1,95 @@ +package com.secure_use_api.token; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import com.google.gson.Gson; +import com.secure_use_api.util.Config; + +public class RefreshToken extends AbstractToken { + static long expirationTime = 2592000000L; // 30 day + protected static SimpleTokenStringHandler tokenStringHandler = new JwtHandler( + Config.getInstance().getSecretRefreshTokenKey()); + + protected final UUID uid; + protected final UUID spouseId; + protected final Date expiresAt; + + public class InnerAccessToken { + public String jti; + public String spouseId; + public Long exp; + } + + public RefreshToken(UUID spouseId) { + this.uid = UUID.randomUUID(); + this.spouseId = spouseId; + this.expiresAt = new Date(System.currentTimeMillis() + expirationTime); + } + + protected RefreshToken(UUID uid, UUID spouseId, Date expiresAt) { + this.uid = uid; + this.spouseId = spouseId; + this.expiresAt = expiresAt; + } + + static public Optional fromTokenString(String tokeString) throws IllegalArgumentException { + Optional payloadString = tokenStringHandler.payloadFromTokenString(tokeString); + + if (payloadString.isPresent()) { + Gson json = new Gson(); + InnerAccessToken payload = json.fromJson(payloadString.get(), InnerAccessToken.class); + + String uidString = payload.jti; + String spouseIdString = payload.spouseId; + Long expiresAtLong = payload.exp; + + if (uidString == null || uidString.isEmpty()) { + throw new IllegalArgumentException("Missing uid"); + } + + if (spouseIdString == null || spouseIdString.isEmpty()) { + throw new IllegalArgumentException("Missing userId"); + } + + if (expiresAtLong == null) { + throw new IllegalArgumentException("expires date is null"); + } + + UUID uid = UUID.fromString(uidString); + UUID spouseId = UUID.fromString(spouseIdString); + Date expiresAtDate = new Date(expiresAtLong * 1000); + + return Optional.of(new RefreshToken(uid, spouseId, expiresAtDate)); + } + + return Optional.empty(); + } + + public String toTokenString() { + Map payload = new HashMap<>(); + + payload.put("jti", this.uid.toString()); + payload.put("spouseId", this.spouseId.toString()); + payload.put("iat", new Date().getTime() / 1000); + payload.put("exp", this.expiresAt.getTime() / 1000); + + return tokenStringHandler.payloadToTokenString(payload); + } + + public UUID getUid() { + return this.uid; + } + + public UUID getSpouseId() { + return this.spouseId; + } + + public Date getExpiresAt() { + return this.expiresAt; + } + +} diff --git a/securety_api/src/main/java/com/secure_use_api/token/SimpleTokenStringHandler.java b/securety_api/src/main/java/com/secure_use_api/token/SimpleTokenStringHandler.java new file mode 100644 index 0000000..e07587d --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/token/SimpleTokenStringHandler.java @@ -0,0 +1,9 @@ +package com.secure_use_api.token; + +import java.util.Map; +import java.util.Optional; + +public interface SimpleTokenStringHandler { + Optional payloadFromTokenString(String tokeString); + String payloadToTokenString(Map payload); +} diff --git a/securety_api/src/main/java/com/secure_use_api/util/Config.java b/securety_api/src/main/java/com/secure_use_api/util/Config.java new file mode 100644 index 0000000..58e6724 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/util/Config.java @@ -0,0 +1,36 @@ +package com.secure_use_api.util; + +import io.github.cdimascio.dotenv.Dotenv; + +public class Config { + // Singleton instance + private static Config instance; + private final Dotenv dotenv; + + private Config() { + dotenv = Dotenv.load(); // Load the .env file + } + + public static Config getInstance() { + if (instance == null) { + instance = new Config(); + } + return instance; + } + + public String getSecretAccessTokenKey() { + return dotenv.get("SECRET_ACCESS_TOKEN_KEY"); + } + + public String getSecretRefreshTokenKey() { + return dotenv.get("SECRET_REFRESH_TOKEN_KEY"); + } + + public double getRateLimitPerSecond() { + return Double.parseDouble(dotenv.get("RATE_LIMIT_PER_SECOND")); + } + + public int getResourceLimitPerDay() { + return Integer.parseInt(dotenv.get("RESOURCE_LIMIT_PER_DAY")); + } +} \ No newline at end of file diff --git a/securety_api/src/main/java/com/secure_use_api/util/HibernateUtil.java b/securety_api/src/main/java/com/secure_use_api/util/HibernateUtil.java new file mode 100644 index 0000000..bfaff90 --- /dev/null +++ b/securety_api/src/main/java/com/secure_use_api/util/HibernateUtil.java @@ -0,0 +1,25 @@ +package com.secure_use_api.util; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; + +public class HibernateUtil { + private static final SessionFactory sessionFactory = buildSessionFactory(); + + private static SessionFactory buildSessionFactory() { + try { + return new Configuration().configure().buildSessionFactory(); + } catch (Throwable ex) { + throw new ExceptionInInitializerError(ex); + } + } + + public static SessionFactory getSessionFactory() { + return sessionFactory; + } + + public static Session getCurrentSession() { + return getSessionFactory().getCurrentSession(); + } +} \ No newline at end of file diff --git a/securety_api/src/main/resources/.DS_Store b/securety_api/src/main/resources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3ea226f23bb9e0951b4d52005b20f2501d7e98ac GIT binary patch literal 6148 zcmeHK&u`N(6n^eXmuN!j0njuF$r9J9bU$EByJTe?xDvz;fJ)LtBap>aldgxVQqJR# z;mTjazq5VM_F!EH;)D?MtDZmm`8~V-USh{YB>IzNlc-HZ9+a`+qqsr1pLIzZ*3tqB zJw`@Dim0ND)RMO|{EG~5?j9g#Bm8pY?PvZ9>cTcgCP$P4Ijbp^FLO1Fa}^_wADRPb z_go8VL>m}I(J}4NCrT*Bc-$NdkRAP4XTn!iJ7hito8&AuT{jcBRQJ$4m zulHRvR+@KOtKO=&=DiO;s%cn7)uil46^WYm)uc#D9UGbs&-eU6dwn+Bezx0@o4qIVj-2gn^*VBA`{{h{d-oqb-hVat zoSzixj7h`mgZD?vU5l5%>swg=X*e!&RUE_4F+P|{=|pFb8CuGacnGhn8A8*EW56+R z>lv_nj<#wL!ggpwL$UU=wC7sPktb$4HBT#o8cxAi{(K zO{lO}3}M1ym)0+^SQ|9qB<$rw*qMdBp$I)Y#+TZiL}1XBjseHOA_FU?+2-^A#jo%G zi$SjC7;p^yR}6^eVR+cblI+>Kv^YL%CG-Q7h5c%SKSEH^M=^Z)C|-wZfn9P17+9 + + + + + + + logs/filter-logs.log + + logs/requests.%d{yyyy-MM-dd}.log + 15 + 1GB + + + %msg%n + + + + + + + + + + + + + \ No newline at end of file diff --git a/securety_api/src/main/resources/templates/hello.html b/securety_api/src/main/resources/templates/hello.html new file mode 100644 index 0000000..8cb9705 --- /dev/null +++ b/securety_api/src/main/resources/templates/hello.html @@ -0,0 +1,12 @@ + + + + + + + +

+ Hello! +

+ + \ No newline at end of file diff --git a/securety_api/src/test/.DS_Store b/securety_api/src/test/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..8077d8a7acaa2aac0df423b804b1ccf1510c9931 GIT binary patch literal 8196 zcmeHM%}*0S6n_IKTM@hRAs-i;G$tO1mJj&|2WzPz#3+OkgaFpvb}1{{oo07i1VYja z2leD1;2+>cZ^n}s4<0;g;>C+s4<5bgo0)BCwh$$LAaPzY^Lu^s-puT8XJ+?p0e~cm z*?xd-08p?nR9mpxM_4;6b<*Hl!iWUpValQz-J&}FR_nZ_K{6m2kPJu$BmtM@ly=qXAVS!V|(29FAEgmK={bmk3TZ;GZf+{9wfrufy4#MS~4IRsAquX?oDtH zG$?@<$ltGF+of+@cwp5}-*5|lp8rxWSf=a6V(&>qW7CO~%}TSaj#5jn=$bxn8XIY~V}jbAX4;0s1I(;}F3Yz~J4-hbv}k8t9-9&kN=ON%)$Z-> zE2G1Gk-^cioxaHS*g&i=GBh~6vlCJ}qnF2L)9Y5=rVquX692e>R`I)9KSHf~J*AXm zThz{>jrli20VufOK^}RrCue73Mb~q9#4WVs!Kn8NaQ6&79@KCb=<}!<92!B^1_nod zk-@-KnVFKmF34_WU3{#pi_oc-)2(O1>e+BxxV^ojQ|;>RNvrL;mRZj^>|Re$i#iFb zXr$G!iz{X-L+u&IDB((3@QnLj%G|=Hwm`tFk-f8Ln%gX(Psg5tcRJ}9hP{+?%lW*8TQ0JBX39rK1k46h@y%0FKxm?_SMbt7(4{WUGIG5T( zX`hIr-dw-wc-rU60&LaXM5#&!4$DBJ2+f{NK-PEOyGno z?-9Ydvs^oZ8ceg&q~=fxIqMJkCH{`i z>~4foy?78TGcfx$vojm!ZP>{$#<(*L_86-(#tcxzk_p2%g6pVDQqq<(Ajdt#(2s-< zPq-b)e_#O5uFQr^u<=>6aDKwEs5fS_!)~W(wY$A})0*}6TTSbr-I>o#V{3c&=zQ=Pjbrhmi0AN4O4-m@z$;92 zHqY)PjzoM9mcqO6A|wWg0b*cv8PGSLQCr;gw)W2zs&K~Tn_(aQX0AIFu3Hao zNq5F=g)|Za#K0m0*!zLhvHqX_-2Y1_8i)a6U^N-wmA>0|AU9oGm$JlKYk}T@qM%={ m@GAr+vJ`_amf{Ae6tG)30kk#d3c&(GKLU~l8i;{EW#AL?j#AwK literal 0 HcmV?d00001 diff --git a/securety_api/src/test/java/com/.DS_Store b/securety_api/src/test/java/com/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6be1c0d5bbbc408f205de4056d5e5bbbb8784026 GIT binary patch literal 6148 zcmeHK!A{#i5SI%7IqKCQ)l)yiuHxLlntp_#wW8 z-+?!~8$?A*Z!N9nNwaUfJF~XmTDu-1Qk_xvJyDa00vL0xf#xURakgiu`JOqjvU3cm zM=_141PlHJ1$gb=+4VZdx;Y_#@ect=(I|`%P{-F8e5m(m(`h4EUR~cg ziZAlZ!d%<8AcgPO;;rWy-N1QKpQ~t4HSB~x0fyR~q;5xc3;PY=H=V;HN=gKig0OeA_E>-ywL%DSHYabVS zt{l5`Qoi_5{?5u*D9YX)^J^PUDs(KZC?E>_r~ub~K$Fk^gJ0MGHb@##Kos~t6;Sp4 zXupGZ^JnX`x8$={!_Hs~j;kC$rogdBF=F{BUV&MlU$X-aJy(v=1IUknl|dR&;EyWs E0A$r*rvLx| literal 0 HcmV?d00001 diff --git a/securety_api/src/test/java/com/secure_use_api/SecureUseAppiApplicationTests.java b/securety_api/src/test/java/com/secure_use_api/SecureUseAppiApplicationTests.java new file mode 100644 index 0000000..9f82515 --- /dev/null +++ b/securety_api/src/test/java/com/secure_use_api/SecureUseAppiApplicationTests.java @@ -0,0 +1,13 @@ +package com.secure_use_api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SecureUseApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/securety_api/src/test/postman/.DS_Store b/securety_api/src/test/postman/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bc42af5a44daaf3aaae80585323fa7b7e970c5eb GIT binary patch literal 6148 zcmeHKyG{c^3>-s>NHi%a_X|$q4~|nPQU?kuAb|pO$)b=Hbo)bmBaGJu>BQ9$LiWi! zUa!56k!~LVq&qxb05bqns-mbdBI+J(I`Lp3QrSm`2Q;|EE1pL>`im}|dxINnIpbz9 z|0SNV#sNFF?D39k*57C~TwSi#hO6D;j!_vsKaJlrI@827-ZL;P?#a*+-e48Acfl1Ovgq4+FYCB&uTTSPbjd!J<9^h!eW2(AI0N z<|N0~u^6(2B9=&RWz=sL7_dk1hAs}$a!tF b_M|rBTE}9jy{NyY6XPOK35g;YI06G-7+olV literal 0 HcmV?d00001 diff --git a/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_collection.json b/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_collection.json new file mode 100644 index 0000000..9a9b7df --- /dev/null +++ b/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_collection.json @@ -0,0 +1,1551 @@ +{ + "info": { + "_postman_id": "53687375-8243-4fe8-98d1-543e1f899f2d", + "name": "Uni.Ba.ApiSecurityTest", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "17943160" + }, + "item": [ + { + "name": "RegisterUserSuccess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test('Response contains required fields: id and actionType', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('id', 'actionType');", + "})", + "", + "pm.test('The id must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.id).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Value should not be empty');", + "})", + "", + "pm.test('ActionType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.actionType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Value should not be empty');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer xyz", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{UserEmail}}\",\n \"password\": \"{{UserPassword}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/register", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "register" + ] + } + }, + "response": [] + }, + { + "name": "RegisterUserFail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.response.to.have.status(400)", + "pm.test('Response status code is 400', function () {", + " pm.expect(pm.response.code).to.eql(400);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer xyz", + "type": "text", + "disabled": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{UserEmail}}\",\n \"password\": \"{{UserPassword}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/register", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "register" + ] + } + }, + "response": [] + }, + { + "name": "HelloAuthenticatedFail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 401', function () {", + " pm.expect(pm.response.code).to.eql(401);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/auth", + "host": [ + "{{Host}}" + ], + "path": [ + "auth" + ] + } + }, + "response": [] + }, + { + "name": "LoginUserFail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.response.to.have.status(400)", + "pm.test('Response status code is 400', function () {", + " pm.expect(pm.response.code).to.eql(400);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{UserEmail}}\",\n \"password\": \"wrongPassword\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/login", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "LoginUserSuccess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test('Response has required fields', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('token', 'refreshToken');", + "})", + "", + "pm.test('Token must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.token).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Token should not be empty');", + "})", + "", + "pm.test('The refreshToken must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.refreshToken).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Value should not be empty');", + "})", + "", + "const accessToken = pm.response.json().token", + "const refreshToken = pm.response.json().refreshToken", + "", + "pm.collectionVariables.set(\"AccessToken\", accessToken)", + "pm.collectionVariables.set(\"RefreshToken\", refreshToken)" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{UserEmail}}\",\n \"password\": \"{{UserPassword}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/login", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "HelloAuthenticatedSuccess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/auth/", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "" + ] + } + }, + "response": [] + }, + { + "name": "RefreshAccessToken", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", function () {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test(\"Response has required fields: token and refreshToken\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('token', 'refreshToken');", + "});", + "", + "pm.test(\"Token is a non-empty string\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.token).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, \"Token should not be empty\");", + "});", + "", + "pm.test(\"RefreshToken must be a non-empty string\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.refreshToken).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, \"Value should not be empty\");", + "});", + "", + "const accessToken = pm.response.json().token", + "const refreshToken = pm.response.json().refreshToken", + "", + "pm.collectionVariables.set(\"AccessToken\", accessToken)", + "pm.collectionVariables.set(\"RefreshToken\", refreshToken)" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"refreshToken\": \"{{RefreshToken}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/login", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "login" + ] + } + }, + "response": [] + }, + { + "name": "HelloAuthenticatedSuccess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/auth/", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "" + ] + } + }, + "response": [] + }, + { + "name": "GetAllApiTokensEmpty", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test('Response content type is application/json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "})", + "", + "pm.test('Response is an array', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('array');", + "})", + "", + "pm.test('The response array must be empty', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('array').that.is.empty;", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/api/getAll", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "getAll" + ] + } + }, + "response": [] + }, + { + "name": "CreateApiToken", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test('Response has required fields: token and refreshToken', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('token', 'refreshToken');", + "})", + "", + "pm.test('Token is a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.token).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Token should not be empty');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "url": { + "raw": "{{Host}}/api/create", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "create" + ] + } + }, + "response": [] + }, + { + "name": "GetAllApiTokensNotEmpty", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Response status code is 200\", function () {", + " pm.expect(pm.response.code).to.equal(200);", + "});", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test(\"Response is an array\", function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('array');", + "});", + "", + "", + "pm.test(\"Response array is not empty\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array').that.is.not.empty;", + "});", + "", + "pm.test(\"Each item in the response array is a non-empty string\", function () {", + " const responseData = pm.response.json();", + " ", + " pm.expect(responseData).to.be.an('array');", + " responseData.forEach(item => {", + " pm.expect(item).to.be.a('string').and.to.have.lengthOf.at.least(1, \"Value should not be empty\");", + " });", + "});", + "", + "const apiToken = pm.response.json()[0]", + "", + "pm.environment.set(\"ApiToken\", apiToken)" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/api/getAll", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "getAll" + ] + } + }, + "response": [] + }, + { + "name": "HelloAuthenticatedApiTokenSuccess", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ApiToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/auth/", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "" + ] + } + }, + "response": [] + }, + { + "name": "DeleteUserFailPermission", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 403', function () {", + " pm.expect(pm.response.code).to.eql(403);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ApiToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer xyz", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{Host}}/auth/delete", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "delete" + ] + } + }, + "response": [] + }, + { + "name": "DeleteApiTokenFailPermission", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "function parseJwt (token) {", + " const base64Url = token.split('.')[1]; // get payload part", + " const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');", + " const jsonPayload = decodeURIComponent(", + " atob(base64)", + " .split('')", + " .map(c => `%${('00' + c.charCodeAt(0).toString(16)).slice(-2)}`)", + " .join('')", + " );", + " return JSON.parse(jsonPayload);", + "}", + "", + "const apiToken = pm.environment.get(\"ApiToken\");", + "", + "const apiTokenId = parseJwt(apiToken).jti;", + "", + "pm.environment.set(\"ApiTokenId\", apiTokenId);" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 403', function () {", + " pm.expect(pm.response.code).to.equal(403);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ApiToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{Host}}/api/delete/{{ApiTokenId}}", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "delete", + "{{ApiTokenId}}" + ] + } + }, + "response": [] + }, + { + "name": "DeleteApiToken", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test('Content-Type is application/json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "})", + "", + "pm.test('Response contains the required fields', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('id', 'actionType');", + "})", + "", + "pm.test('ActionType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.actionType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'ActionType should not be empty');", + "})", + "", + "pm.environment.unset(\"ApiToken\")", + "pm.environment.unset(\"ApiTokenId\")", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{Host}}/api/delete/{{ApiTokenId}}", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "delete", + "{{ApiTokenId}}" + ] + } + }, + "response": [] + }, + { + "name": "GetAllApiTokensEmpty", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test('Response content type is application/json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "})", + "", + "pm.test('Response is an array', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('array');", + "})", + "", + "pm.test('The response array must be empty', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('array').that.is.empty;", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/api/getAll", + "host": [ + "{{Host}}" + ], + "path": [ + "api", + "getAll" + ] + } + }, + "response": [] + }, + { + "name": "HelloAuthenticatedApiTokenFail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 401', function () {", + " pm.expect(pm.response.code).to.equal(401);", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ApiToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/auth/", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "" + ] + } + }, + "response": [] + }, + { + "name": "ResourceLimit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test('Response content type is application/json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "})", + "", + "pm.test('Response has required fields: time and unitType', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('time', 'unitType');", + "})", + "", + "pm.test('Time is not zero and a non-negative integer', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.time).to.be.a('number').and.to.be.at.least(1);", + "})", + "", + "pm.test('UnitType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.unitType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'UnitType should not be empty');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/resourceLimit", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "resourceLimit" + ] + } + }, + "response": [] + }, + { + "name": "LongRunningTask", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(`Request duration is at least 20 seconds`, function () {", + " pm.expect(pm.response.responseTime).to.be.at.least(20000);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/longRun", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "longRun" + ] + } + }, + "response": [] + }, + { + "name": "ResourceLimit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test('Response content type is application/json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "})", + "", + "pm.test('Response has required fields: time and unitType', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('time', 'unitType');", + "})", + "", + "pm.test('Time is not zero and a non-negative integer', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.time).to.be.a('number').and.to.be.at.least(1);", + "})", + "", + "pm.test('UnitType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.unitType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'UnitType should not be empty');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/resourceLimit", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "resourceLimit" + ] + } + }, + "response": [] + }, + { + "name": "LongRunningTaskSecond", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(`Request duration is at least 20 seconds`, function () {", + " pm.expect(pm.response.responseTime).to.be.at.least(20000);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/longRun", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "longRun" + ] + } + }, + "response": [] + }, + { + "name": "LongRunningTaskThird", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(`Request duration is at least 20 seconds`, function () {", + " pm.expect(pm.response.responseTime).to.be.at.least(20000);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/longRun", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "longRun" + ] + } + }, + "response": [] + }, + { + "name": "LongRunningTaskFailPermission", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 403', function () {", + " pm.expect(pm.response.code).to.eql(403);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})", + "", + "pm.test(`Request duration is less than 20 seconds`, function () {", + " pm.expect(pm.response.responseTime).to.be.not.greaterThanOrEqual(20000);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/longRun", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "longRun" + ] + } + }, + "response": [] + }, + { + "name": "ResourceLimit", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test('Response contains required fields: time and unitType', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('time', 'unitType');", + "})", + "", + "pm.test('Time is a zero or a negativ integer', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.time).to.be.a('number').and.to.be.lessThanOrEqual(0);", + "})", + "", + "pm.test('UnitType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.unitType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'Value should not be empty');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{Host}}/user/resourceLimit", + "host": [ + "{{Host}}" + ], + "path": [ + "user", + "resourceLimit" + ] + } + }, + "response": [] + }, + { + "name": "DeleteUser", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 200', function () {", + " pm.expect(pm.response.code).to.equal(200);", + "})", + "", + "pm.test(\"Response Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/json');", + "});", + "", + "pm.test('Response contains the required fields', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData).to.have.all.keys('id', 'actionType');", + "})", + "", + "pm.test('ActionType must be a non-empty string', function () {", + " const responseData = pm.response.json();", + " pm.expect(responseData).to.be.an('object');", + " pm.expect(responseData.actionType).to.exist.and.to.be.a('string').and.to.have.lengthOf.at.least(1, 'ActionType should not be empty');", + "})", + "", + "pm.collectionVariables.unset(\"AccessToken\")", + "pm.collectionVariables.unset(\"RefreshToken\")" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer xyz", + "type": "text", + "disabled": true + } + ], + "url": { + "raw": "{{Host}}/auth/delete", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "delete" + ] + } + }, + "response": [] + }, + { + "name": "LoginUserFail", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Response status code is 400', function () {", + " pm.expect(pm.response.code).to.equal(400);", + "})", + "", + "pm.test('Response content type is application/problem+json', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.equal('application/problem+json');", + "})" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{UserEmail}}\",\n \"password\": \"{{UserPassword}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{Host}}/auth/login", + "host": [ + "{{Host}}" + ], + "path": [ + "auth", + "login" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "Host", + "value": "127.0.0.1:8080" + }, + { + "key": "UserEmail", + "value": "apiUser@hello.com" + }, + { + "key": "UserPassword", + "value": "SecurePassword" + }, + { + "key": "AccessToken", + "value": "" + }, + { + "key": "RefreshToken", + "value": "" + }, + { + "key": "ApiToken", + "value": "" + }, + { + "key": "ApiTokenId", + "value": "" + } + ] +} \ No newline at end of file diff --git a/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_test_run.json b/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_test_run.json new file mode 100644 index 0000000..409cf7c --- /dev/null +++ b/securety_api/src/test/postman/Uni.Ba.ApiSecurityTest.postman_test_run.json @@ -0,0 +1,1784 @@ +{ + "id": "95fff205-c3c4-445e-82aa-5af17b63047c", + "name": "Uni.Ba.ApiSecurityTest", + "timestamp": "2025-09-28T00:10:08.992Z", + "collection_id": "17943160-53687375-8243-4fe8-98d1-543e1f899f2d", + "folder_id": 0, + "environment_id": "0", + "totalPass": 400, + "delay": 0, + "persist": true, + "status": "finished", + "startedAt": "2025-09-28T00:04:22.434Z", + "totalFail": 0, + "results": [ + { + "id": "15986188-ab75-40be-8bdb-79f41bf87a1a", + "name": "RegisterUserSuccess", + "url": "127.0.0.1:8080/auth/register", + "time": 2584, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response contains required fields: id and actionType": { + "pass": 5, + "fail": 0 + }, + "The id must be a non-empty string": { + "pass": 5, + "fail": 0 + }, + "ActionType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 3115, + 1840, + 1760, + 2088, + 2584 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: id and actionType": true, + "The id must be a non-empty string": true, + "ActionType must be a non-empty string": true + } + ] + }, + { + "id": "6e7bde1a-79e0-4ffe-8dea-8c91a902b1d5", + "name": "RegisterUserFail", + "url": "127.0.0.1:8080/auth/register", + "time": 87, + "responseCode": { + "code": 400, + "name": "Bad Request" + }, + "tests": { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 400": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 135, + 29, + 25, + 30, + 87 + ], + "allTests": [ + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + } + ] + }, + { + "id": "8a3bc1df-b648-438c-80a8-d06ef6a9a382", + "name": "HelloAuthenticatedFail", + "url": "127.0.0.1:8080/auth", + "time": 30, + "responseCode": { + "code": 401, + "name": "Unauthorized" + }, + "tests": { + "Response status code is 401": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 401": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 144, + 26, + 15, + 18, + 30 + ], + "allTests": [ + { + "Response status code is 401": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 401": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 401": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 401": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 401": true, + "Response content type is application/problem+json": true + } + ] + }, + { + "id": "a28f4dde-4e32-43be-9914-b97c1058e54d", + "name": "LoginUserFail", + "url": "127.0.0.1:8080/auth/login", + "time": 1648, + "responseCode": { + "code": 400, + "name": "Bad Request" + }, + "tests": { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 400": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 2518, + 2451, + 1853, + 1601, + 1648 + ], + "allTests": [ + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + } + ] + }, + { + "id": "fbd12ddf-939c-4266-aa77-2442fe55c2df", + "name": "LoginUserSuccess", + "url": "127.0.0.1:8080/auth/login", + "time": 1622, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response has required fields": { + "pass": 5, + "fail": 0 + }, + "Token must be a non-empty string": { + "pass": 5, + "fail": 0 + }, + "The refreshToken must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 2556, + 1604, + 1606, + 1891, + 1622 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields": true, + "Token must be a non-empty string": true, + "The refreshToken must be a non-empty string": true + } + ] + }, + { + "id": "ecb38d56-b671-47a9-9adf-886211b4b5a0", + "name": "HelloAuthenticatedSuccess", + "url": "127.0.0.1:8080/auth/", + "time": 22, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 86, + 18, + 26, + 18, + 22 + ], + "allTests": [ + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + } + ] + }, + { + "id": "7fdf3024-02e5-43e3-89a6-12e0f0474377", + "name": "RefreshAccessToken", + "url": "127.0.0.1:8080/auth/login", + "time": 29, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response has required fields: token and refreshToken": { + "pass": 5, + "fail": 0 + }, + "Token is a non-empty string": { + "pass": 5, + "fail": 0 + }, + "RefreshToken must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 80, + 40, + 47, + 43, + 29 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true, + "RefreshToken must be a non-empty string": true + } + ] + }, + { + "id": "72d7b0c0-7c32-4c81-9210-576734e41136", + "name": "HelloAuthenticatedSuccess", + "url": "127.0.0.1:8080/auth/", + "time": 13, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 43, + 21, + 31, + 19, + 13 + ], + "allTests": [ + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + } + ] + }, + { + "id": "e609169d-9e3a-4867-8aaa-1edb98a2ce7c", + "name": "GetAllApiTokensEmpty", + "url": "127.0.0.1:8080/api/getAll", + "time": 30, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response is an array": { + "pass": 5, + "fail": 0 + }, + "The response array must be empty": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 68, + 29, + 33, + 22, + 30 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + } + ] + }, + { + "id": "9c133ae9-c512-49bd-b34b-472f2d7898de", + "name": "CreateApiToken", + "url": "127.0.0.1:8080/api/create", + "time": 18, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response has required fields: token and refreshToken": { + "pass": 5, + "fail": 0 + }, + "Token is a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 152, + 28, + 26, + 35, + 18 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response has required fields: token and refreshToken": true, + "Token is a non-empty string": true + } + ] + }, + { + "id": "031eeba4-d034-4cb1-8616-f6c96824e98d", + "name": "GetAllApiTokensNotEmpty", + "url": "127.0.0.1:8080/api/getAll", + "time": 20, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response is an array": { + "pass": 5, + "fail": 0 + }, + "Response array is not empty": { + "pass": 5, + "fail": 0 + }, + "Each item in the response array is a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 30, + 26, + 36, + 33, + 20 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response is an array": true, + "Response array is not empty": true, + "Each item in the response array is a non-empty string": true + } + ] + }, + { + "id": "b7a045b5-29a4-4bea-bf91-2c12cc2cba86", + "name": "HelloAuthenticatedApiTokenSuccess", + "url": "127.0.0.1:8080/auth/", + "time": 25, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 102, + 24, + 42, + 25, + 25 + ], + "allTests": [ + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + }, + { + "Response status code is 200": true + } + ] + }, + { + "id": "ff1f6e83-0c74-4652-9c50-eb981a014315", + "name": "DeleteUserFailPermission", + "url": "127.0.0.1:8080/auth/delete", + "time": 32, + "responseCode": { + "code": 403, + "name": "Forbidden" + }, + "tests": { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 403": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 528, + 22, + 43, + 23, + 32 + ], + "allTests": [ + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + } + ] + }, + { + "id": "3c71f887-7c04-44c5-80f0-beedaaca8aae", + "name": "DeleteApiTokenFailPermission", + "url": "127.0.0.1:8080/api/delete/b2686d54-52e6-4800-803d-996312c4c434", + "time": 30, + "responseCode": { + "code": 403, + "name": "Forbidden" + }, + "tests": { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 403": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 100, + 16, + 181, + 13, + 30 + ], + "allTests": [ + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true + } + ] + }, + { + "id": "673dd18c-8143-4423-9734-71dcc4e76425", + "name": "DeleteApiToken", + "url": "127.0.0.1:8080/api/delete/b2686d54-52e6-4800-803d-996312c4c434", + "time": 29, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response contains the required fields": { + "pass": 5, + "fail": 0 + }, + "ActionType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 149, + 26, + 17, + 19, + 29 + ], + "allTests": [ + { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + } + ] + }, + { + "id": "4a4a3c7e-38a6-46cd-ae02-6161f6cd99b9", + "name": "GetAllApiTokensEmpty", + "url": "127.0.0.1:8080/api/getAll", + "time": 21, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response is an array": { + "pass": 5, + "fail": 0 + }, + "The response array must be empty": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 268, + 16, + 11, + 18, + 21 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response is an array": true, + "The response array must be empty": true + } + ] + }, + { + "id": "e507a9a5-2232-4d6d-9cd8-8d17b92eda67", + "name": "HelloAuthenticatedApiTokenFail", + "url": "127.0.0.1:8080/auth/", + "time": 11, + "responseCode": { + "code": 401, + "name": "Unauthorized" + }, + "tests": { + "Response status code is 401": true + }, + "testPassFailCounts": { + "Response status code is 401": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 66, + 13, + 22, + 18, + 11 + ], + "allTests": [ + { + "Response status code is 401": true + }, + { + "Response status code is 401": true + }, + { + "Response status code is 401": true + }, + { + "Response status code is 401": true + }, + { + "Response status code is 401": true + } + ] + }, + { + "id": "6e232a81-8191-43b7-b9a2-84742b7fd851", + "name": "ResourceLimit", + "url": "127.0.0.1:8080/user/resourceLimit", + "time": 28, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response has required fields: time and unitType": { + "pass": 5, + "fail": 0 + }, + "Time is not zero and a non-negative integer": { + "pass": 5, + "fail": 0 + }, + "UnitType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 81, + 23, + 19, + 21, + 28 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + } + ] + }, + { + "id": "59381005-fa78-4884-a913-2dd7eb478179", + "name": "LongRunningTask", + "url": "127.0.0.1:8080/user/longRun", + "time": 21742, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Request duration is at least 20 seconds": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 20188, + 20031, + 20038, + 20038, + 21742 + ], + "allTests": [ + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + } + ] + }, + { + "id": "6e7756d0-501e-4aa6-addd-58e621113dbc", + "name": "ResourceLimit", + "url": "127.0.0.1:8080/user/resourceLimit", + "time": 603, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response has required fields: time and unitType": { + "pass": 5, + "fail": 0 + }, + "Time is not zero and a non-negative integer": { + "pass": 5, + "fail": 0 + }, + "UnitType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 71, + 84, + 37, + 35, + 603 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response content type is application/json": true, + "Response has required fields: time and unitType": true, + "Time is not zero and a non-negative integer": true, + "UnitType must be a non-empty string": true + } + ] + }, + { + "id": "c4310c60-8b5d-453f-8393-a5aa5ad70648", + "name": "LongRunningTaskSecond", + "url": "127.0.0.1:8080/user/longRun", + "time": 20192, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Request duration is at least 20 seconds": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 20051, + 20058, + 20041, + 20047, + 20192 + ], + "allTests": [ + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + } + ] + }, + { + "id": "c2812d37-ceeb-48be-bb04-256ca59993b4", + "name": "LongRunningTaskThird", + "url": "127.0.0.1:8080/user/longRun", + "time": 20145, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Request duration is at least 20 seconds": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 20072, + 20034, + 20102, + 20079, + 20145 + ], + "allTests": [ + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + }, + { + "Response status code is 200": true, + "Request duration is at least 20 seconds": true + } + ] + }, + { + "id": "1443c123-d96e-4559-86d6-0a5cf76c5abf", + "name": "LongRunningTaskFailPermission", + "url": "127.0.0.1:8080/user/longRun", + "time": 44, + "responseCode": { + "code": 403, + "name": "Forbidden" + }, + "tests": { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + }, + "testPassFailCounts": { + "Response status code is 403": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + }, + "Request duration is less than 20 seconds": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 37, + 33, + 60, + 21, + 44 + ], + "allTests": [ + { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + }, + { + "Response status code is 403": true, + "Response content type is application/problem+json": true, + "Request duration is less than 20 seconds": true + } + ] + }, + { + "id": "7630449b-537e-4c0b-912e-57cbbef7c341", + "name": "ResourceLimit", + "url": "127.0.0.1:8080/user/resourceLimit", + "time": 30, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response contains required fields: time and unitType": { + "pass": 5, + "fail": 0 + }, + "Time is a zero or a negativ integer": { + "pass": 5, + "fail": 0 + }, + "UnitType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 43, + 32, + 22, + 28, + 30 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains required fields: time and unitType": true, + "Time is a zero or a negativ integer": true, + "UnitType must be a non-empty string": true + } + ] + }, + { + "id": "a8c1bd51-d747-47ae-971a-3910ade260ab", + "name": "DeleteUser", + "url": "127.0.0.1:8080/auth/delete", + "time": 52, + "responseCode": { + "code": 200, + "name": "OK" + }, + "tests": { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + "testPassFailCounts": { + "Response status code is 200": { + "pass": 5, + "fail": 0 + }, + "Response Content-Type is application/json": { + "pass": 5, + "fail": 0 + }, + "Response contains the required fields": { + "pass": 5, + "fail": 0 + }, + "ActionType must be a non-empty string": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 78, + 50, + 55, + 36, + 52 + ], + "allTests": [ + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + }, + { + "Response status code is 200": true, + "Response Content-Type is application/json": true, + "Response contains the required fields": true, + "ActionType must be a non-empty string": true + } + ] + }, + { + "id": "0055c096-c7af-41f7-857c-5c764624e06a", + "name": "LoginUserFail", + "url": "127.0.0.1:8080/auth/login", + "time": 69, + "responseCode": { + "code": 400, + "name": "Bad Request" + }, + "tests": { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + "testPassFailCounts": { + "Response status code is 400": { + "pass": 5, + "fail": 0 + }, + "Response content type is application/problem+json": { + "pass": 5, + "fail": 0 + } + }, + "times": [ + 106, + 43, + 58, + 44, + 69 + ], + "allTests": [ + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + }, + { + "Response status code is 400": true, + "Response content type is application/problem+json": true + } + ] + } + ], + "count": 5, + "totalTime": 339109, + "collection": { + "requests": [ + { + "id": "15986188-ab75-40be-8bdb-79f41bf87a1a", + "method": "POST" + }, + { + "id": "6e7bde1a-79e0-4ffe-8dea-8c91a902b1d5", + "method": "POST" + }, + { + "id": "8a3bc1df-b648-438c-80a8-d06ef6a9a382", + "method": "GET" + }, + { + "id": "a28f4dde-4e32-43be-9914-b97c1058e54d", + "method": "POST" + }, + { + "id": "fbd12ddf-939c-4266-aa77-2442fe55c2df", + "method": "POST" + }, + { + "id": "ecb38d56-b671-47a9-9adf-886211b4b5a0", + "method": "GET" + }, + { + "id": "7fdf3024-02e5-43e3-89a6-12e0f0474377", + "method": "PUT" + }, + { + "id": "72d7b0c0-7c32-4c81-9210-576734e41136", + "method": "GET" + }, + { + "id": "e609169d-9e3a-4867-8aaa-1edb98a2ce7c", + "method": "GET" + }, + { + "id": "9c133ae9-c512-49bd-b34b-472f2d7898de", + "method": "POST" + }, + { + "id": "031eeba4-d034-4cb1-8616-f6c96824e98d", + "method": "GET" + }, + { + "id": "b7a045b5-29a4-4bea-bf91-2c12cc2cba86", + "method": "GET" + }, + { + "id": "ff1f6e83-0c74-4652-9c50-eb981a014315", + "method": "DELETE" + }, + { + "id": "3c71f887-7c04-44c5-80f0-beedaaca8aae", + "method": "DELETE" + }, + { + "id": "673dd18c-8143-4423-9734-71dcc4e76425", + "method": "DELETE" + }, + { + "id": "4a4a3c7e-38a6-46cd-ae02-6161f6cd99b9", + "method": "GET" + }, + { + "id": "e507a9a5-2232-4d6d-9cd8-8d17b92eda67", + "method": "GET" + }, + { + "id": "6e232a81-8191-43b7-b9a2-84742b7fd851", + "method": "GET" + }, + { + "id": "59381005-fa78-4884-a913-2dd7eb478179", + "method": "GET" + }, + { + "id": "6e7756d0-501e-4aa6-addd-58e621113dbc", + "method": "GET" + }, + { + "id": "c4310c60-8b5d-453f-8393-a5aa5ad70648", + "method": "GET" + }, + { + "id": "c2812d37-ceeb-48be-bb04-256ca59993b4", + "method": "GET" + }, + { + "id": "1443c123-d96e-4559-86d6-0a5cf76c5abf", + "method": "GET" + }, + { + "id": "7630449b-537e-4c0b-912e-57cbbef7c341", + "method": "GET" + }, + { + "id": "a8c1bd51-d747-47ae-971a-3910ade260ab", + "method": "DELETE" + }, + { + "id": "0055c096-c7af-41f7-857c-5c764624e06a", + "method": "POST" + } + ] + } +} \ No newline at end of file diff --git a/securety_api/src/test/resources/.gitkeep b/securety_api/src/test/resources/.gitkeep new file mode 100644 index 0000000..e69de29 From 59037cb09223e1d25ed9d45b8873b41d96ff5ad6 Mon Sep 17 00:00:00 2001 From: Florian Rohr Date: Mon, 15 Dec 2025 03:02:38 +0100 Subject: [PATCH 2/6] Integrated Security features (backed them in) --- .../src/main/java/com/.DS_Store => .DS_Store | Bin 6148 -> 8196 bytes .env | 4 + .idea/misc.xml | 8 + .vscode/README.md | 14 -- pom.xml | 2 +- .../java/com => secure-use-api}/.DS_Store | Bin 6148 -> 6148 bytes {securety_api => secure-use-api}/.env | 0 {use-api => secure-use-api}/Dockerfile | 0 .../docker-compose.yml | 0 secure-use-api/logs/filter-logs.log | 146 ++++++++++++++++++ {use-api => secure-use-api}/pom.xml | 99 +++++++++++- .../src/.DS_Store | Bin .../src/main/.DS_Store | Bin .../src/main/java/.DS_Store | Bin .../main/java/org/tzi/use/SecureUseApi.java | 6 +- .../CustomAuthenticationEntryPoint.java | 2 +- .../exceptions/CustomExceptionHandler.java | 2 +- .../exceptions/ExceptionResponse.java | 2 +- .../api_security}/exceptions/Exceptions.java | 2 +- .../api_security}/model/ApiTokenModel.java | 2 +- .../model/RefreshTokenModel.java | 2 +- .../model/UserAdditionsModel.java | 2 +- .../api_security}/model/UserComposite.java | 2 +- .../api_security}/model/UserMetaModel.java | 2 +- .../model/UserSecurityModel.java | 2 +- .../repository/ApiTokenRepository.java | 4 +- .../repository/RefreshTokenRepository.java | 4 +- .../repository/UserAdditionsRepository.java | 4 +- .../repository/UserMetaRepository.java | 4 +- .../repository/UserSecurityRepository.java | 4 +- .../rest/controller/ApiTokenController.java | 20 +-- .../controller/AuthenticationController.java | 42 +++-- .../rest/controller/UserController.java | 14 +- .../api_security}/rest/dto/IdActionDto.java | 2 +- .../rest/dto/RefreshTokenDto.java | 2 +- .../rest/dto/ResourceLimitLeftDto.java | 2 +- .../use/api_security}/rest/dto/TokensDto.java | 2 +- .../api_security}/rest/dto/UserLoginDto.java | 2 +- .../rest/dto/UserRegistrationDto.java | 2 +- .../rest/middleware/AuditLogMiddleware.java | 4 +- .../middleware/AuthenticationMiddleware.java | 21 ++- .../middleware/AuthorizationMiddleware.java | 10 +- .../ExceptionHandlerMiddleware.java | 4 +- .../rest/middleware/RateLimitMiddleware.java | 6 +- .../middleware/ResourceLimitMiddleware.java | 8 +- .../rest/service/ApiTokenService.java | 10 +- .../rest/service/AuthenticationService.java | 18 +-- .../rest/service/RefreshTokenService.java | 14 +- .../rest/service/UserService.java | 12 +- .../security/CustomAuthProvider.java | 12 +- .../api_security}/security/RequireScope.java | 2 +- .../tzi/use/api_security}/security/Role.java | 2 +- .../security/SecurityConfig.java | 19 +-- .../security/UserAuthentication.java | 2 +- .../security/UserDetailsImpl.java | 4 +- .../use/api_security}/security/WebConfig.java | 6 +- .../api_security}/token/AbstractToken.java | 2 +- .../use/api_security}/token/AccessToken.java | 6 +- .../tzi/use/api_security}/token/ApiToken.java | 4 +- .../use/api_security}/token/JwtHandler.java | 2 +- .../api_security}/token/PasetoHandler.java | 2 +- .../use/api_security}/token/RefreshToken.java | 4 +- .../token/SimpleTokenStringHandler.java | 2 +- .../tzi/use/api_security}/util/Config.java | 2 +- .../use/api_security}/util/HibernateUtil.java | 2 +- .../use_logic}/DTO/AggregationTypeDTO.java | 2 +- .../use/use_logic}/DTO/AssociationDTO.java | 2 +- .../tzi/use/use_logic}/DTO/AttributeDTO.java | 2 +- .../org/tzi/use/use_logic}/DTO/ClassDTO.java | 2 +- .../tzi/use/use_logic}/DTO/InvariantDTO.java | 2 +- .../org/tzi/use/use_logic}/DTO/ModelDTO.java | 8 +- .../tzi/use/use_logic}/DTO/OperationDTO.java | 2 +- .../use_logic}/DTO/PrePostConditionDTO.java | 2 +- .../use_logic}/GlobalExceptionHandler.java | 2 +- .../org/tzi/use/use_logic}/OpenApiConfig.java | 2 +- .../tzi/use/use_logic}/UseModelFacade.java | 4 +- .../entities/AggregationTypeNTT.java | 2 +- .../use_logic}/entities/AssociationNTT.java | 2 +- .../use/use_logic}/entities/AttributeNTT.java | 2 +- .../tzi/use/use_logic}/entities/ClassNTT.java | 2 +- .../use/use_logic}/entities/InvariantNTT.java | 2 +- .../tzi/use/use_logic}/entities/ModelNTT.java | 4 +- .../use/use_logic}/entities/OperationNTT.java | 2 +- .../entities/PrePostConditionNTT.java | 2 +- .../use_logic}/mapper/AssociationMapper.java | 6 +- .../use_logic}/mapper/AttributeMapper.java | 6 +- .../use/use_logic}/mapper/ClassMapper.java | 6 +- .../use_logic}/mapper/InvariantMapper.java | 6 +- .../use/use_logic}/mapper/ModelMapper.java | 6 +- .../use_logic}/mapper/OperationMapper.java | 6 +- .../mapper/PrePostConditionMapper.java | 6 +- .../use/use_logic}/repository/ModelRepo.java | 4 +- .../rest/controller/ClassController.java | 10 +- .../rest/controller/ModelController.java | 7 +- .../rest/services/ClassService.java | 22 +-- .../rest/services/ModelService.java | 12 +- .../src/main/resources/.DS_Store | Bin .../src/main/resources/application.properties | 18 +++ .../src/main/resources/docker-compose.yml | 0 .../src/main/resources/logback-spring.xml | 6 +- .../src/test/.DS_Store | Bin .../src/test/java/.DS_Store | Bin .../org/tzi/use/RestApiControllerTest.java | 12 +- .../src/test/postman/.DS_Store | Bin ...Ba.ApiSecurityTest.postman_collection.json | 0 ...i.Ba.ApiSecurityTest.postman_test_run.json | 0 ...se-webapi-extended.postman_collection.json | 0 .../use-webapi.postman_collection.json | 0 .../src/test/resources/.gitkeep | 0 securety_api/pom.xml | 113 -------------- .../src/main/resources/application.properties | 11 -- .../src/main/resources/templates/hello.html | 12 -- .../SecureUseAppiApplicationTests.java | 13 -- .../org/tzi/use/UseWebAPIApplication.java | 11 -- 114 files changed, 507 insertions(+), 421 deletions(-) rename securety_api/src/main/java/com/.DS_Store => .DS_Store (60%) create mode 100644 .env delete mode 100644 .vscode/README.md rename {securety_api/src/test/java/com => secure-use-api}/.DS_Store (89%) rename {securety_api => secure-use-api}/.env (100%) rename {use-api => secure-use-api}/Dockerfile (100%) rename {use-api => secure-use-api}/docker-compose.yml (100%) create mode 100644 secure-use-api/logs/filter-logs.log rename {use-api => secure-use-api}/pom.xml (77%) rename {securety_api => secure-use-api}/src/.DS_Store (100%) rename {securety_api => secure-use-api}/src/main/.DS_Store (100%) rename {securety_api => secure-use-api}/src/main/java/.DS_Store (100%) rename securety_api/src/main/java/com/secure_use_api/SecureUseApiApplication.java => secure-use-api/src/main/java/org/tzi/use/SecureUseApi.java (59%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/exceptions/CustomAuthenticationEntryPoint.java (94%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/exceptions/CustomExceptionHandler.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/exceptions/ExceptionResponse.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/exceptions/Exceptions.java (92%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/ApiTokenModel.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/RefreshTokenModel.java (95%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/UserAdditionsModel.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/UserComposite.java (94%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/UserMetaModel.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/model/UserSecurityModel.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/repository/ApiTokenRepository.java (81%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/repository/RefreshTokenRepository.java (86%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/repository/UserAdditionsRepository.java (90%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/repository/UserMetaRepository.java (76%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/repository/UserSecurityRepository.java (70%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/controller/ApiTokenController.java (81%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/controller/AuthenticationController.java (78%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/controller/UserController.java (75%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/IdActionDto.java (91%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/RefreshTokenDto.java (92%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/ResourceLimitLeftDto.java (93%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/TokensDto.java (92%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/UserLoginDto.java (94%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/dto/UserRegistrationDto.java (94%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/AuditLogMiddleware.java (96%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/AuthenticationMiddleware.java (80%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/AuthorizationMiddleware.java (90%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/ExceptionHandlerMiddleware.java (88%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/RateLimitMiddleware.java (89%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/middleware/ResourceLimitMiddleware.java (93%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/service/ApiTokenService.java (87%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/service/AuthenticationService.java (83%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/service/RefreshTokenService.java (79%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/rest/service/UserService.java (90%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/CustomAuthProvider.java (88%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/RequireScope.java (88%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/Role.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/SecurityConfig.java (84%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/UserAuthentication.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/UserDetailsImpl.java (94%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/security/WebConfig.java (88%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/AbstractToken.java (88%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/AccessToken.java (96%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/ApiToken.java (92%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/JwtHandler.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/PasetoHandler.java (93%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/RefreshToken.java (97%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/token/SimpleTokenStringHandler.java (84%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/util/Config.java (95%) rename {securety_api/src/main/java/com/secure_use_api => secure-use-api/src/main/java/org/tzi/use/api_security}/util/HibernateUtil.java (94%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/AggregationTypeDTO.java (90%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/AssociationDTO.java (93%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/AttributeDTO.java (86%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/ClassDTO.java (91%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/InvariantDTO.java (88%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/ModelDTO.java (73%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/OperationDTO.java (88%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/DTO/PrePostConditionDTO.java (89%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/GlobalExceptionHandler.java (99%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/OpenApiConfig.java (96%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/UseModelFacade.java (98%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/AggregationTypeNTT.java (89%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/AssociationNTT.java (92%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/AttributeNTT.java (84%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/ClassNTT.java (91%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/InvariantNTT.java (86%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/ModelNTT.java (90%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/OperationNTT.java (86%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/entities/PrePostConditionNTT.java (87%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/AssociationMapper.java (65%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/AttributeMapper.java (65%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/ClassMapper.java (65%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/InvariantMapper.java (65%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/ModelMapper.java (77%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/OperationMapper.java (65%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/mapper/PrePostConditionMapper.java (66%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/repository/ModelRepo.java (71%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/rest/controller/ClassController.java (96%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/rest/controller/ModelController.java (99%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/rest/services/ClassService.java (87%) rename {use-api/src/main/java/org/tzi/use => secure-use-api/src/main/java/org/tzi/use/use_logic}/rest/services/ModelService.java (96%) rename {securety_api => secure-use-api}/src/main/resources/.DS_Store (100%) rename {use-api => secure-use-api}/src/main/resources/application.properties (57%) rename {use-api => secure-use-api}/src/main/resources/docker-compose.yml (100%) rename {securety_api => secure-use-api}/src/main/resources/logback-spring.xml (81%) rename {securety_api => secure-use-api}/src/test/.DS_Store (100%) rename {securety_api => secure-use-api}/src/test/java/.DS_Store (100%) rename {use-api => secure-use-api}/src/test/java/org/tzi/use/RestApiControllerTest.java (95%) rename {securety_api => secure-use-api}/src/test/postman/.DS_Store (100%) rename {securety_api/src/test/postman => secure-use-api/src/test/postman/api_security}/Uni.Ba.ApiSecurityTest.postman_collection.json (100%) rename {securety_api/src/test/postman => secure-use-api/src/test/postman/api_security}/Uni.Ba.ApiSecurityTest.postman_test_run.json (100%) rename {use-api/src/it/java/org.tzi.use/postman_collection => secure-use-api/src/test/postman/use_logic}/use-webapi-extended.postman_collection.json (100%) rename {use-api/src/it/java/org.tzi.use/postman_collection => secure-use-api/src/test/postman/use_logic}/use-webapi.postman_collection.json (100%) rename {securety_api => secure-use-api}/src/test/resources/.gitkeep (100%) delete mode 100644 securety_api/pom.xml delete mode 100644 securety_api/src/main/resources/application.properties delete mode 100644 securety_api/src/main/resources/templates/hello.html delete mode 100644 securety_api/src/test/java/com/secure_use_api/SecureUseAppiApplicationTests.java delete mode 100644 use-api/src/main/java/org/tzi/use/UseWebAPIApplication.java diff --git a/securety_api/src/main/java/com/.DS_Store b/.DS_Store similarity index 60% rename from securety_api/src/main/java/com/.DS_Store rename to .DS_Store index 6be1c0d5bbbc408f205de4056d5e5bbbb8784026..edc9948eb27e09f64ab6c2711a255530ef932320 100644 GIT binary patch literal 8196 zcmeHMO-~a+7=8x|-3l0DG~uAhCf-cJ@}WY!lu}|yPz*~XMosBg%Es+((+?{lBt2{5 z-J^FCuf~H{|AGI-c+lsaU2u0w3nCGWGs(=m-F=>UXP$XyW@k%8BGW2O6HO42fy{P% z1jQkVpL6a?1HqkZumXId3YBSpw zHBmoYzeO8p!78mP=+_E5Mh<^_poW6`rr?Ir4(@FWr4z*tR1|Cs{+DT!tSC+@@;;=< zAUwCI8AS>997e^d23(b<=p{Z)d!lP4%orLAzlhCF{w&D-{5jR4;?#nH-8?_*YJ{D?@-Dsv#U&X*+Y-l)c#ElW- znYHb;t!BR2YGm^DC;V%}^PIx|b-wV(F1K^zmzO-ZnYZ0?T@vh48IY%℘Sr8L#D* z8j@@I4~&G7$c<0z?#|BNNt)BCTYE`!cVQ-#H0Ne-@9iawE7xz{UC%vts;>76`XPy6 zQ=u{c_d&@K*0+yNW!frw)fzK~-n>g6g|B+qY@mrT|LObH&+RWfxBB|1dZpjy)V4$I zdJ7Ln_;_K}UB`1d`dN!h{bB-|1&zwkX^sgg_dEj)#vpQdnRmWM!+04R1(wv!_ z`@=XpR$rBI8V!w;jWu=W@Odj&weXzp$vbN;PifBf?4P1vyx(faYkVZ4M?>g_RK7|D zMDb2^mE?6i!ZQwdMMOPT*(jcOy$JqSt14isYcSTN2%6{i$-x1{xHO~2`F>Yf$jJf acaZ-2hk#Rm&(!<>xqD~V`@fu9{feJJkHC`v delta 238 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGjUEV6q~50D9Q|y2a6Rkq%y=alrj`Eq;4!+ z&S(mf;REp)l7XT~vOrad3 + +