Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
263cbc9
dev/security/ Добавил зависимости для security и jwt
AlexanderGarifullin Dec 12, 2024
c290acb
dev/security/ Добавил responses и payloads для security
AlexanderGarifullin Dec 12, 2024
4d23bc1
dev/security/ Добавил контроллер для security
AlexanderGarifullin Dec 12, 2024
7127c90
dev/security/ Добавил ошибки для security
AlexanderGarifullin Dec 12, 2024
8974a25
dev/security/ Удалил роль admin
AlexanderGarifullin Dec 12, 2024
2fde9f1
dev/security/ Добавил сервис для UserController
AlexanderGarifullin Dec 12, 2024
e77bae5
dev/security/ Добавил сервис для jwt
AlexanderGarifullin Dec 12, 2024
addd383
dev/security/ Добавил бины для security
AlexanderGarifullin Dec 12, 2024
77bd01e
dev/security/ Добавил UserDetails
AlexanderGarifullin Dec 12, 2024
3971b93
dev/security/ Добавил spring-boot-starter-validation
AlexanderGarifullin Dec 12, 2024
ced19a3
dev/security/ Добавил ошибки для токена jwt
AlexanderGarifullin Dec 12, 2024
477c6c9
dev/security/ Добавил фильтр с jwt
AlexanderGarifullin Dec 12, 2024
a76df46
dev/security/ Добавил TokenService
AlexanderGarifullin Dec 12, 2024
bb1c798
dev/security/ Добавил security filter
AlexanderGarifullin Dec 12, 2024
058c385
dev/security/ Добавил бины для security
AlexanderGarifullin Dec 12, 2024
e15c003
dev/security/ Добавил jwt.yml в gitignore
AlexanderGarifullin Dec 12, 2024
262673f
dev/security/ Добавил jwt.yml в проект
AlexanderGarifullin Dec 12, 2024
34c15e5
dev/security/ Проверка gitignore
AlexanderGarifullin Dec 12, 2024
e4bd522
dev/security/ попытка добавить файл из gitignore
AlexanderGarifullin Dec 12, 2024
8cf51dc
dev/security/ Исправил код по checkstyle
AlexanderGarifullin Dec 12, 2024
c4b418f
dev/security/ Поменял русскую букву c на английскую
AlexanderGarifullin Dec 12, 2024
3e176e4
dev/security/ Поменял русскую букву c на английскую в БД и удалил лиш…
AlexanderGarifullin Dec 12, 2024
34eddd8
dev/security/ Для login теперь нужен jwt токен
AlexanderGarifullin Dec 12, 2024
4a97117
dev/security/ Удалил поле c_time в таблицах бд
AlexanderGarifullin Dec 12, 2024
f757570
dev/security/ Разрешил /auth/login всем пользователям
AlexanderGarifullin Dec 12, 2024
761befb
dev/security/ Исправил классы нет под checkStyle
AlexanderGarifullin Dec 12, 2024
8cb3ca2
dev/security/ Написал ControllerAdvice для security
AlexanderGarifullin Dec 12, 2024
0c0cc48
dev/security/ Написал unit тест для UserController
AlexanderGarifullin Dec 12, 2024
f1780ca
dev/security/ Исправил SecurityExceptionHandler под checkstyle
AlexanderGarifullin Dec 12, 2024
204072f
dev/security/ Написал unit тест для TokenService
AlexanderGarifullin Dec 12, 2024
520526e
dev/security/ Написал unit тест для MyUserDetailsService
AlexanderGarifullin Dec 12, 2024
bc4d68b
dev/security/ Написал unit тест для AuthenticationService
AlexanderGarifullin Dec 12, 2024
3b5c7d9
dev/security/ Добавил базовый интеграционный тест
AlexanderGarifullin Dec 13, 2024
87e9f7b
dev/security/ Добавил позитивные интеграционные тесты для UserController
AlexanderGarifullin Dec 13, 2024
5b8b48d
dev/security/ Пытаюсь исправить ci
AlexanderGarifullin Dec 13, 2024
1dfe1f7
dev/security/ Пытаюсь исправить ci 2
AlexanderGarifullin Dec 13, 2024
fef905b
dev/security/ Пытаюсь исправить ci 3
AlexanderGarifullin Dec 13, 2024
7d41dc2
dev/security/ Пытаюсь исправить ci 4
AlexanderGarifullin Dec 13, 2024
ef73046
dev/security/ Пытаюсь исправить ci 5
AlexanderGarifullin Dec 13, 2024
026ab31
dev/security/ Написал интеграционные тесты на негативные сценарии
AlexanderGarifullin Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
run: chmod +x gradlew

- name: Build project
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
JWT_SHORT: ${{ secrets.JWT_SHORT }}
JWT_LONG: ${{ secrets.JWT_LONG }}
run: ./gradlew bootJar

test:
Expand All @@ -50,6 +54,10 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Verify test resources
run: ls src/test/resources


- name: Run tests
run: ./gradlew test

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/

### yml ###
src/main/resources/jwt.yml
src/main/resources/test.yml
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ checkstyle {
dependencies {
// spring
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'

// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
Expand All @@ -37,6 +39,11 @@ dependencies {
implementation 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.testcontainers:junit-jupiter:1.20.1'
Expand Down
5 changes: 4 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ services:
depends_on:
- db
environment:
- SPRING_PROFILES_ACTIVE=prod
- POSTGRES_CONTAINER_NAME=db
- POSTGRES_PORT=5432
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=123
- POSTGRES_DB=CodeforcesLocalDB
- SPRING_PROFILES_ACTIVE=prod
- JWT_SECRET=mysuperSecretPasswordformysuperbestAppwithsuperloginandBisness123Good2wow3magic1
- JWT_SHORT=10m
- JWT_LONG=30d
volumes:
db_data:
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.cf.cfteam.advicers.security;

import com.cf.cfteam.exceptions.security.InvalidTwoFactorCodeException;
import com.cf.cfteam.exceptions.security.TokenRevokedException;
import com.cf.cfteam.exceptions.security.UserAlreadyRegisterException;
import com.cf.cfteam.exceptions.security.UserNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class SecurityExceptionHandler {

private static final String LOGIN = "login";
private static final String TOKEN = "token";
private static final String UNEXPECTED_ERROR = "unexpected.error";

@ExceptionHandler(UserAlreadyRegisterException.class)
public ResponseEntity<Object> handleUserAlreadyRegisterException(UserAlreadyRegisterException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, ex.getLogin()));
}

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(LOGIN, ex.getLogin()));
}

@ExceptionHandler(TokenRevokedException.class)
public ResponseEntity<Object> handleTokenRevokedException(TokenRevokedException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, ex.getToken()));
}

@ExceptionHandler(InvalidTwoFactorCodeException.class)
public ResponseEntity<Object> handleInvalidTwoFactorCodeException(InvalidTwoFactorCodeException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGenericException(Exception ex) {
return buildErrorResponse(UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, null);
}

private ResponseEntity<Object> buildErrorResponse(String message, HttpStatus status, Map<String, Object> details) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", status.value());
errorResponse.put("error", status.getReasonPhrase());
errorResponse.put("message", message);

if (details != null) {
errorResponse.put("details", details);
}

return new ResponseEntity<>(errorResponse, status);
}
}
67 changes: 67 additions & 0 deletions src/main/java/com/cf/cfteam/auth/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.cf.cfteam.auth;

import com.cf.cfteam.exceptions.security.TokenRevokedException;
import com.cf.cfteam.services.security.JwtService;
import com.cf.cfteam.services.security.TokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;


import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
public static final String BEARER_PREFIX = "Bearer ";

private final JwtService jwtService;
private final TokenService tokenService;

private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException, TokenRevokedException {
var authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}

var jwt = authHeader.substring(BEARER_PREFIX.length());

if (tokenService.isTokenRevoked(jwt)) throw new TokenRevokedException(jwt);

var userLogin = jwtService.extractUserLogin(jwt);

var userDetails = userDetailsService.loadUserByUsername(userLogin);

UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails,
userDetails.getPassword(),
userDetails.getAuthorities());

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(authToken);
}

filterChain.doFilter(request, response);
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/cf/cfteam/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.cf.cfteam.config;

import com.cf.cfteam.services.security.MyUserDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
@RequiredArgsConstructor
public class AppConfig {

private final MyUserDetailsService myUserDetailsService;

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

@Bean
public AuthenticationProvider authenticationProvider() {
var authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}

@Bean
public UserDetailsService userDetailsService() {
return myUserDetailsService;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/cf/cfteam/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.cf.cfteam.config;

import com.cf.cfteam.auth.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;


import java.util.List;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(request -> {
var corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOriginPatterns(List.of("*"));
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
corsConfiguration.setAllowedHeaders(List.of("*"));
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}))
.authorizeHttpRequests(request -> request
.requestMatchers("/auth/register", "/auth/login").permitAll()
// .requestMatchers("/**").hasRole("User")
.anyRequest().authenticated())
.sessionManagement(sessionManagementConfigurer ->
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.cf.cfteam.controllers.security;

import com.cf.cfteam.services.security.AuthenticationService;
import com.cf.cfteam.transfer.payloads.security.AuthenticationPayload;
import com.cf.cfteam.transfer.payloads.security.ChangePasswordPayload;
import com.cf.cfteam.transfer.payloads.security.RegistrationPayload;
import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class UserController {

private final AuthenticationService authenticationService;

@PostMapping("/register")
public JwtAuthenticationResponse register(@RequestBody RegistrationPayload registrationRequest) {
return authenticationService.register(registrationRequest);
}

@PostMapping("/login")
public JwtAuthenticationResponse login(@RequestBody AuthenticationPayload authenticationPayload,
Authentication authentication) {
return authenticationService.login(authenticationPayload);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(Authentication authentication) {
authenticationService.logout(authentication);
return ResponseEntity.ok().build();
}

@PatchMapping("change-password")
public ResponseEntity<Void> changePassword(
@RequestBody ChangePasswordPayload changePasswordRequest,
Authentication authentication
) {
authenticationService.changePassword(changePasswordRequest, authentication);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.cf.cfteam.exceptions.security;

public class InvalidTwoFactorCodeException extends RuntimeException {
public InvalidTwoFactorCodeException() {
super("two-factor.code_invalid");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class TokenNotFoundException extends RuntimeException {
private final String token;

public TokenNotFoundException(String token) {
super("token.not_found");
this.token = token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class TokenRevokedException extends RuntimeException {

private final String token;

public TokenRevokedException(String token) {
super("token.is_revoked");
this.token = token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class UserAlreadyRegisterException extends RuntimeException {

private final String login;

public UserAlreadyRegisterException(String login) {
super("login.already_register");
this.login = login;
}
}
Loading