Initial commit.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package org.codiki.exposition.configuration;
|
||||
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
||||
import org.codiki.domain.exception.LoginFailureException;
|
||||
import org.codiki.domain.exception.RefreshTokenDoesNotExistException;
|
||||
import org.codiki.domain.exception.RefreshTokenExpiredException;
|
||||
import org.codiki.domain.exception.UserDoesNotExistException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalControllerExceptionHandler {
|
||||
|
||||
@ResponseStatus(BAD_REQUEST)
|
||||
@ExceptionHandler(LoginFailureException.class)
|
||||
public void handleLoginFailureException() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@ResponseStatus(NOT_FOUND)
|
||||
@ExceptionHandler(UserDoesNotExistException.class)
|
||||
public void handleUserDoesNotExistException() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@ResponseStatus(NOT_FOUND)
|
||||
@ExceptionHandler(RefreshTokenDoesNotExistException.class)
|
||||
public void handleRefreshTokenDoesNotExistException() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@ResponseStatus(UNAUTHORIZED)
|
||||
@ExceptionHandler(RefreshTokenExpiredException.class)
|
||||
public void handleRefreshTokenExpiredException() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.codiki.exposition.configuration.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
|
||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||
import org.codiki.application.security.JwtService;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
|
||||
this.jwtService = jwtService;
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
String authorizationHeader = request.getHeader(AUTHORIZATION);
|
||||
|
||||
if (!isEmpty(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX)) {
|
||||
String token = authorizationHeader.substring(BEARER_PREFIX.length());
|
||||
String username = jwtService.extractUsername(token);
|
||||
|
||||
if (!isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||
if (jwtService.isValid(token) && userDetails.getUsername().equals(username)) {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.codiki.exposition.configuration.security;
|
||||
|
||||
import static org.springframework.http.HttpMethod.GET;
|
||||
import static org.springframework.http.HttpMethod.OPTIONS;
|
||||
import static org.springframework.http.HttpMethod.POST;
|
||||
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
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.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import static jakarta.servlet.DispatcherType.FORWARD;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(securedEnabled = true)
|
||||
public class SecurityConfiguration {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity httpSecurity,
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter
|
||||
) throws Exception {
|
||||
httpSecurity
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(Customizer.withDefaults())
|
||||
.exceptionHandling(configurer -> configurer
|
||||
.authenticationEntryPoint((request, response, authException) -> response.sendError(SC_UNAUTHORIZED))
|
||||
.accessDeniedHandler((request, response, accessDeniedException) -> response.sendError(SC_FORBIDDEN))
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.sessionManagement(customizer -> customizer.sessionCreationPolicy(STATELESS))
|
||||
.authorizeHttpRequests(requests -> requests
|
||||
.dispatcherTypeMatchers(FORWARD).permitAll()
|
||||
.requestMatchers(
|
||||
GET,
|
||||
"/api/health/check",
|
||||
"/error"
|
||||
).permitAll()
|
||||
.requestMatchers(
|
||||
POST,
|
||||
"/api/users/login",
|
||||
"/api/users/refresh-token"
|
||||
).permitAll()
|
||||
.requestMatchers(OPTIONS).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
|
||||
return httpSecurity.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.codiki.exposition.healthcheck;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/health")
|
||||
public class HealthCheckController {
|
||||
@GetMapping("/check")
|
||||
public String healthCheck() {
|
||||
return "Ok";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.codiki.exposition.user;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.codiki.application.security.annotation.AllowedToAdmins;
|
||||
import org.codiki.application.security.annotation.AllowedToAnonymous;
|
||||
import org.codiki.application.user.UserUseCases;
|
||||
import org.codiki.domain.user.model.User;
|
||||
import org.codiki.domain.user.model.UserAuthenticationData;
|
||||
import org.codiki.exposition.user.model.LoginRequest;
|
||||
import org.codiki.exposition.user.model.LoginResponse;
|
||||
import org.codiki.exposition.user.model.RefreshTokenRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
private final UserUseCases userUseCases;
|
||||
|
||||
public UserController(UserUseCases userUseCases) {
|
||||
this.userUseCases = userUseCases;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@AllowedToAnonymous
|
||||
public LoginResponse login(@RequestBody LoginRequest request) {
|
||||
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.id(), request.password());
|
||||
return new LoginResponse(userAuthenticationData);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@AllowedToAdmins
|
||||
public List<User> findAll() {
|
||||
return userUseCases.findAll();
|
||||
}
|
||||
|
||||
@PostMapping("/refresh-token")
|
||||
public LoginResponse refreshToken(@RequestBody RefreshTokenRequest request) {
|
||||
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.refreshTokenValue());
|
||||
return new LoginResponse(userAuthenticationData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.codiki.exposition.user.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record LoginRequest(
|
||||
UUID id,
|
||||
String password
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.codiki.exposition.user.model;
|
||||
|
||||
import org.codiki.domain.user.model.UserAuthenticationData;
|
||||
|
||||
public record LoginResponse(
|
||||
String tokenType,
|
||||
String accessToken,
|
||||
String refreshToken
|
||||
) {
|
||||
public LoginResponse(UserAuthenticationData userAuthenticationData) {
|
||||
this(
|
||||
userAuthenticationData.tokenType(),
|
||||
userAuthenticationData.accessToken(),
|
||||
userAuthenticationData.refreshToken().value().toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.codiki.exposition.user.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record RefreshTokenRequest(
|
||||
UUID refreshTokenValue
|
||||
) {
|
||||
}
|
||||
Reference in New Issue
Block a user