diff --git a/pom.xml b/pom.xml index d36bbc0..9d5be99 100755 --- a/pom.xml +++ b/pom.xml @@ -33,10 +33,10 @@ org.springframework.boot spring-boot-starter-web - - - - + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot diff --git a/src/main/java/org/codiki/CodikiApplication.java b/src/main/java/org/codiki/CodikiApplication.java index 8203781..46bea92 100755 --- a/src/main/java/org/codiki/CodikiApplication.java +++ b/src/main/java/org/codiki/CodikiApplication.java @@ -3,9 +3,11 @@ package org.codiki; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication @EnableAutoConfiguration +@ComponentScan(basePackages = "org.codiki") public class CodikiApplication { public static void main(String[] args) { diff --git a/src/main/java/org/codiki/account/AccountController.java b/src/main/java/org/codiki/account/AccountController.java index 67a65ec..babcdff 100755 --- a/src/main/java/org/codiki/account/AccountController.java +++ b/src/main/java/org/codiki/account/AccountController.java @@ -1,7 +1,7 @@ package org.codiki.account; import java.io.IOException; -import java.util.Optional; +import java.security.Principal; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -9,8 +9,11 @@ import javax.servlet.http.HttpServletResponse; import org.codiki.core.entities.dto.PasswordWrapperDTO; import org.codiki.core.entities.dto.UserDTO; import org.codiki.core.entities.persistence.User; -import org.codiki.core.security.TokenService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -21,40 +24,20 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/account") public class AccountController { - - private static final String HEADER_TOKEN = "token"; - @Autowired private AccountService accountService; - @Autowired - private TokenService tokenService; - - /** - * Log in the user in request body. - * - * @param pUser - * The user to connect. - * @param response - * The reponse injected by Spring. - * @return The connected user object. - * @throws IOException - * If credentials are bad. - */ @PostMapping("/login") - public UserDTO login(@RequestBody UserDTO pUser, HttpServletResponse response) throws IOException { - return accountService.checkCredentials(response, pUser); + public UserDTO login(@RequestBody final User pUser) throws BadCredentialsException { + return new UserDTO(accountService.authenticate(pUser)); } - /** - * Log out the user. - * - * @param pRequest - * The request injected by Spring. - */ @GetMapping("/logout") - public void logout(HttpServletRequest pRequest) { - tokenService.removeUser(pRequest.getHeader(HEADER_TOKEN)); + public void logout(final HttpServletRequest request, final HttpServletResponse response) { + final Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if(auth != null) { + new SecurityContextLogoutHandler().logout(request, response, auth); + } } /** @@ -74,13 +57,15 @@ public class AccountController { @PutMapping("/changePassword") public void changePassword(@RequestBody final PasswordWrapperDTO pPasswordWrapper, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); - if(connectedUser.isPresent()) { - accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse); - } else { - pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); - } + final HttpServletResponse pResponse, + final Principal pPrincipal) throws IOException { +// final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); +// if(connectedUser.isPresent()) { +// accountService.changePassword(connectedUser.get(), pPasswordWrapper, pResponse); +// } else { +// pResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); +// } + } @PostMapping("/signin") @@ -90,7 +75,7 @@ public class AccountController { @PutMapping("/") public void update(@RequestBody final UserDTO pUser, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { - accountService.updateUser(pUser, pRequest, pResponse); + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { + accountService.updateUser(pUser, pRequest, pResponse, pPrincipal); } } diff --git a/src/main/java/org/codiki/account/AccountService.java b/src/main/java/org/codiki/account/AccountService.java index c921696..bc9e72f 100755 --- a/src/main/java/org/codiki/account/AccountService.java +++ b/src/main/java/org/codiki/account/AccountService.java @@ -1,13 +1,13 @@ package org.codiki.account; import java.io.IOException; +import java.security.Principal; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import javax.naming.AuthenticationException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -17,53 +17,41 @@ import org.codiki.core.entities.dto.UserDTO; import org.codiki.core.entities.persistence.User; import org.codiki.core.repositories.ImageRepository; import org.codiki.core.repositories.UserRepository; -import org.codiki.core.security.TokenService; +import org.codiki.core.security.CustomAuthenticationProvider; import org.codiki.core.services.FileUploadService; +import org.codiki.core.services.UserService; import org.codiki.core.utils.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service public class AccountService { + + @Autowired + private CustomAuthenticationProvider authenticationProvider; + + @Autowired + private UserService userService; @Autowired private UserRepository userRepository; - @Autowired - private TokenService tokenService; - @Autowired private FileUploadService fileUploadService; @Autowired private ImageRepository imageRepository; - /** - * Check the user credentials and generate him a token if they are correct. - * - * @param pUser - * The user sent from client. - * @return The user populated with the generated token. - * @throws IOException - * If the credentials are bad. - * @throws AuthenticationException - * If the credentials are wrong. - */ - public UserDTO checkCredentials(HttpServletResponse pResponse, UserDTO pUser) throws IOException { - UserDTO result = null; + public User authenticate(final User pUser) throws BadCredentialsException { + final User user = userService.checkCredentials(pUser.getEmail(), pUser.getPassword()); - Optional user = userRepository.findByEmail(pUser.getEmail()); + authenticationProvider.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())); - if(user.isPresent() && StringUtils.compareHash(pUser.getPassword(), user.get().getPassword())) { - tokenService.addUser(user.get()); - result = new UserDTO(user.get(), true); - } else { - pResponse.sendError(HttpServletResponse.SC_FORBIDDEN); - } - - return result; + return user; } public void changePassword(final User pUser, final PasswordWrapperDTO pPasswordWrapper, @@ -85,11 +73,11 @@ public class AccountService { } } - public String uploadFile(final MultipartFile pFile, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public String uploadFile(final MultipartFile pFile, final HttpServletRequest pRequest, + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { final String avatarFileName = fileUploadService.uploadProfileImage(pFile); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { final Optional userFromDb = userRepository.findById(connectedUser.get().getId()); if(userFromDb.isPresent()) { @@ -109,10 +97,11 @@ public class AccountService { return fileUploadService.loadAvatar(pAvatarFileName); } - public List getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public List getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse, + final Principal pPrincipal) throws IOException { List result = new LinkedList<>(); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { result = imageRepository.getByUserId(connectedUser.get().getId()) .stream().map(ImageDTO::new).collect(Collectors.toList()); @@ -142,8 +131,8 @@ public class AccountService { } public void updateUser(final UserDTO pUser, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { - final Optional connectedUserOpt = tokenService.getAuthenticatedUserByToken(pRequest); + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { + final Optional connectedUserOpt = userService.getUserByPrincipal(pPrincipal); if(connectedUserOpt.isPresent()) { final User connectedUser = connectedUserOpt.get(); diff --git a/src/main/java/org/codiki/core/AbstractFilter.java b/src/main/java/org/codiki/core/AbstractFilter.java deleted file mode 100755 index ae361fc..0000000 --- a/src/main/java/org/codiki/core/AbstractFilter.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.codiki.core; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Pattern; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.codiki.core.security.Route; -import org.codiki.core.utils.StringUtils; -import org.springframework.http.HttpMethod; - -/** - * Base class for all filters of the application.
- *
- * The children classes have to implements the method - * {@link AbstractFilter#getClass()} to set the URLs filtered (with all or some - * http methods), and the method - * {@link AbstractFilter#filter(HttpServletRequest, ServletResponse, FilterChain)} - * to define the filter processing. - * - * @author Takiguchi - * - */ -public abstract class AbstractFilter implements Filter { - - /** Regex url path prefix for method {@link this#isRequestFiltered(String)}. */ - private static final String PREFIX_URL_PATH = "https?:\\/\\/.*(:\\d{0,5})?"; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // Do nothing - } - - /** - * Returns the list of routes which will be processed by the filter. - * - * @return The routes. - */ - protected abstract List getRoutes(); - - /** - * Filter actions for its processing. - * - * @param request - * The http request. - * @param response - * The response. - * @param chain - * The chain. - */ - protected abstract void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - HttpServletRequest httpRequest = (HttpServletRequest) request; - - if(isRequestFiltered(httpRequest.getRequestURL().toString(), httpRequest.getMethod())) { - filter(httpRequest, (HttpServletResponse) response, chain); - } else { - chain.doFilter(request, response); - } - } - - /** - * Check if the url is allowed with the given method in parameters. - * - * @param pRequestUrl - * The url request. - * @param pRequestMethod - * The http method of the request. - * @return {@code true} if the url is allowed with the method, {@code false} - * otherwise. - */ - boolean isRequestFiltered(final String pRequestUrl, final String pRequestMethod) { - boolean result = false; - - for(final Route route : getRoutes()) { - /* - * Check urls matching, and if the method of the route isn't set, all methods - * are allowed. Otherwise, we check the methods too. - */ - if(Pattern.matches(StringUtils.concat(PREFIX_URL_PATH, route.getUrl()), pRequestUrl)) { - if(!route.getMethod().isPresent() || isMethodFiltered(route, pRequestMethod)) { - result = true; - break; - } - } - } - - return result; - } - - /** - * Checks if the route do filter the method in parameters. - * - * @param pRoute - * The registered route. - * @param pRequestMethod - * The http method to check with the registered route. - */ - boolean isMethodFiltered(final Route pRoute, final String pRequestMethod) { - boolean result = false; - - if(pRoute.getMethod().isPresent()) { - for(final HttpMethod routeMethod : pRoute.getMethod().get()) { - if(routeMethod.name().equals(pRequestMethod)) { - result = true; - break; - } - } - } - - return result; - } - - @Override - public void destroy() { - // Do nothing - } -} diff --git a/src/main/java/org/codiki/core/entities/dto/UserDTO.java b/src/main/java/org/codiki/core/entities/dto/UserDTO.java index ced140a..4cb0f39 100755 --- a/src/main/java/org/codiki/core/entities/dto/UserDTO.java +++ b/src/main/java/org/codiki/core/entities/dto/UserDTO.java @@ -21,8 +21,6 @@ public class UserDTO { private Role role; - private String token; - public UserDTO() { super(); } @@ -38,9 +36,6 @@ public class UserDTO { public UserDTO(final User pUser, final boolean pWithToken) { this(pUser); - if(pWithToken) { - token = pUser.getToken().getValue(); - } } public String getKey() { @@ -98,12 +93,4 @@ public class UserDTO { public void setRole(Role role) { this.role = role; } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } } diff --git a/src/main/java/org/codiki/core/entities/persistence/User.java b/src/main/java/org/codiki/core/entities/persistence/User.java index 5a66591..6de2f68 100755 --- a/src/main/java/org/codiki/core/entities/persistence/User.java +++ b/src/main/java/org/codiki/core/entities/persistence/User.java @@ -20,7 +20,6 @@ import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; -import org.codiki.core.entities.security.Token; import org.hibernate.annotations.Generated; import org.hibernate.annotations.GenerationTime; @@ -69,15 +68,11 @@ public class User implements Serializable { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private List listImages; - /** Authentication token. */ - private transient Token token; - /* ******************* */ /* Constructors */ /* ******************* */ public User() { super(); - token = new Token(); } /* ******************* */ @@ -184,9 +179,4 @@ public class User implements Serializable { public void setListImages(List listImages) { this.listImages = listImages; } - - public Token getToken() { - return token; - } - } diff --git a/src/main/java/org/codiki/core/security/AuthenticationFilter.java b/src/main/java/org/codiki/core/security/AuthenticationFilter.java deleted file mode 100755 index e7ab021..0000000 --- a/src/main/java/org/codiki/core/security/AuthenticationFilter.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.codiki.core.security; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.codiki.core.AbstractFilter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpMethod; -import org.springframework.stereotype.Component; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -public class AuthenticationFilter extends AbstractFilter { - - private static final String HTTP_OPTIONS = "OPTIONS"; - - private static final String HEADER_TOKEN = "token"; - - @Autowired - private TokenService tokenService; - - @Override - protected List getRoutes() { - return Arrays.asList( - new Route("\\/api\\/posts\\/myPosts"), - new Route("\\/api\\/posts\\/preview"), - new Route("\\/api\\/posts\\/", HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE), - new Route("\\/api\\/account\\/changePassword"), - new Route("\\/api\\/account\\/", HttpMethod.PUT) - ); - } - - @Override - protected void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if(HTTP_OPTIONS.equals(request.getMethod()) || tokenService.isUserConnected(request.getHeader(HEADER_TOKEN))) { - chain.doFilter(request, response); - } else { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } - } - -} diff --git a/src/main/java/org/codiki/core/security/CustomAuthenticationProvider.java b/src/main/java/org/codiki/core/security/CustomAuthenticationProvider.java new file mode 100755 index 0000000..e0fb2ab --- /dev/null +++ b/src/main/java/org/codiki/core/security/CustomAuthenticationProvider.java @@ -0,0 +1,33 @@ +package org.codiki.core.security; + +import java.util.ArrayList; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +public class CustomAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + // Creation of the authentication bean with its roles + Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getName(), + authentication.getCredentials(), new ArrayList()); + + // Set the auth bean in spring security context + SecurityContextHolder.getContext().setAuthentication(auth); + + return auth; + } + + @Override + public boolean supports(Class authentication) { + return authentication.equals(UsernamePasswordAuthenticationToken.class); + } + +} diff --git a/src/main/java/org/codiki/core/security/Route.java b/src/main/java/org/codiki/core/security/Route.java deleted file mode 100755 index 65dcd6a..0000000 --- a/src/main/java/org/codiki/core/security/Route.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.codiki.core.security; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import org.springframework.http.HttpMethod; - -/** - * Route for filter matching. - * - * @author Takiguchi - * - */ -public class Route { - /** The regex to match urls. */ - private String url; - /** The http method to match. Use a {@link Optional#empty()} to match all methods. */ - private Optional> method; - - /** - * Instanciate a vierge route. - */ - public Route() { - super(); - url = ""; - method = Optional.empty(); - } - - /** - * Instanciate a route for all http methods. - * - * @param pUrl - * The regex to match urls. - */ - public Route(final String pUrl) { - this(); - this.url = pUrl; - } - - /** - * Instanciate a route for methods given in parameters - * - * @param pUrl - * The regex to match urls. - * @param pMethod - * The http method to match. Use a {@link Optional#empty()} to match - * all methods. - */ - public Route(final String pUrl, final HttpMethod... pMethods) { - this(pUrl); - this.method = Optional.of(Arrays.asList(pMethods)); - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public Optional> getMethod() { - return method; - } - - public void setMethod(HttpMethod pMethods) { - this.method = Optional.of(Arrays.asList(pMethods)); - } - -} diff --git a/src/main/java/org/codiki/core/security/SecurityConfiguration.java b/src/main/java/org/codiki/core/security/SecurityConfiguration.java new file mode 100755 index 0000000..5f52e46 --- /dev/null +++ b/src/main/java/org/codiki/core/security/SecurityConfiguration.java @@ -0,0 +1,65 @@ +package org.codiki.core.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; + + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Order(SecurityProperties.BASIC_AUTH_ORDER) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private static final String XSRF_REPOSITORY_HEADER_NAME = "X-XSRF-TOKEN"; + @Autowired + private CustomAuthenticationProvider authenticationProvider; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(authenticationProvider); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .antMatchers("/api/account/login", "/api/account/logout").permitAll() + .antMatchers( + "/api/images/uploadAvatar", + "/api/images/myImages", + "/api/posts/myPosts" + ).authenticated() + .antMatchers( + HttpMethod.GET, + "/api/categories", + "/api/images", + "/api/posts", + "/api/categories/**", + "/api/images/**", + "/api/posts/**" + ).permitAll() + .anyRequest().authenticated() + .and() + .addFilterAfter(new XSRFTokenFilter(), CsrfFilter.class) + .csrf() + .csrfTokenRepository(xsrfTokenRepository()); + http.httpBasic(); + http.csrf().disable(); + } + + private CsrfTokenRepository xsrfTokenRepository() { + HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); + repository.setHeaderName(XSRF_REPOSITORY_HEADER_NAME); + return repository; + } +} \ No newline at end of file diff --git a/src/main/java/org/codiki/core/security/TokenService.java b/src/main/java/org/codiki/core/security/TokenService.java deleted file mode 100755 index de45d5c..0000000 --- a/src/main/java/org/codiki/core/security/TokenService.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.codiki.core.security; - -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; - -import javax.servlet.http.HttpServletRequest; - -import org.codiki.core.entities.persistence.User; -import org.springframework.stereotype.Service; - -@Service -public class TokenService { - /** Map of connected users. */ - private static final Map connectedUsers; - - private static final String HEADER_TOKEN = "token"; - - private static final long INTERVAL_USER_CLEANING = 5; - - private static final long INTERVAL_USER_CLEANING_VAL = INTERVAL_USER_CLEANING * 60 * 1000; - - private Date lastUsersCleaning; - - /** - * Class constructor - */ - static { - connectedUsers = new TreeMap<>(); - } - - /** - * Check if the token matches to a user session, and if it is still valid. - * - * @param pToken - * The token to check. - * @return {@code true} if the token is still valid, {@code false} - * otherwise. - */ - public boolean isUserConnected(final String pToken) { - boolean result = false; - - if (pToken != null && connectedUsers.containsKey(pToken)) { - if (connectedUsers.get(pToken).getToken().isValid()) { - result = true; - } else { - connectedUsers.remove(pToken); - } - } - - // clear all the expired sessions - final Date now = new Date(); - if(lastUsersCleaning == null || now.getTime() - lastUsersCleaning.getTime() >= INTERVAL_USER_CLEANING_VAL) { - new Thread(this::clearExpiredUsers).start(); - lastUsersCleaning = now; - } - - - return result; - } - - /** - * Remove from the connected users map all the elements which their token is - * expired. - */ - private void clearExpiredUsers() { - synchronized (this) { - List usersToRemove = new LinkedList<>(); - connectedUsers.entrySet().stream().forEach(user -> { - if(!user.getValue().getToken().isValid()) { - usersToRemove.add(user.getValue()); - } - }); - usersToRemove.stream().map(User::getKey).forEach(connectedUsers::remove); - } - } - - /** - * Add the user to the connected users map. - * - * @param pUser - * The user to add. - */ - public void addUser(final User pUser) { - if(connectedUsers.get(pUser.getToken().getValue()) == null) { - connectedUsers.put(pUser.getToken().getValue(), pUser); - } - } - - /** - * Refresh the user token last access date in the token service. - * - * @param pToken - * The user token. - */ - public void refreshUserToken(final String pToken) { - final User user = connectedUsers.get(pToken); - if(user != null) { - user.getToken().setLastAccess(); - } - } - - /** - * Remove the user to the connected users map. - * - * @param pUser - * The user to remove. - */ - public void removeUser(final User pUser) { - removeUser(pUser.getToken().getValue()); - } - - /** - * Remove the user associated to the token given in parameters, from the - * connected users map. - * - * @param pToken - * The user to delete token. - */ - public void removeUser(final String pToken) { - if(pToken != null && connectedUsers.containsKey(pToken)) { - connectedUsers.remove(pToken); - } - } - - public Optional getAuthenticatedUserByToken(final HttpServletRequest pRequest) { - return Optional.ofNullable(connectedUsers.get(pRequest.getHeader(HEADER_TOKEN))); - } -} diff --git a/src/main/java/org/codiki/core/security/XSRFTokenFilter.java b/src/main/java/org/codiki/core/security/XSRFTokenFilter.java new file mode 100755 index 0000000..1675084 --- /dev/null +++ b/src/main/java/org/codiki/core/security/XSRFTokenFilter.java @@ -0,0 +1,34 @@ +package org.codiki.core.security; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +public class XSRFTokenFilter extends OncePerRequestFilter { + private static final String XSRF_TOKEN_PATH = "/"; + private static final String XSRF_TOKEN = "XSRF-TOKEN"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + if(csrf != null) { + Cookie cookie = WebUtils.getCookie(request, XSRF_TOKEN); + String token = csrf.getToken(); + if(cookie == null || token != null && !token.equals(cookie.getValue())) { + cookie = new Cookie(XSRF_TOKEN, token); + cookie.setPath(XSRF_TOKEN_PATH); + response.addCookie(cookie); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/org/codiki/core/services/UserService.java b/src/main/java/org/codiki/core/services/UserService.java new file mode 100755 index 0000000..a4e784d --- /dev/null +++ b/src/main/java/org/codiki/core/services/UserService.java @@ -0,0 +1,37 @@ +package org.codiki.core.services; + +import java.security.Principal; +import java.util.Optional; + +import org.codiki.core.entities.persistence.User; +import org.codiki.core.repositories.UserRepository; +import org.codiki.core.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +public class UserService { + private static final String MSG_BAD_CREDENTIALS = "Adresse email ou mot de passe incorrect."; + + @Autowired + private UserRepository userRepository; + + public User checkCredentials(final String email, final String password) throws BadCredentialsException { + final Optional optUser = userRepository.findByEmail(email); + + // If no user exists with the given email of if the given password doesn't match + // to the user password, we throw an exception. + if(!optUser.isPresent() || !StringUtils.compareHash(password, optUser.get().getPassword())) { + throw new BadCredentialsException(MSG_BAD_CREDENTIALS); + } + + return optUser.get(); + } + + public Optional getUserByPrincipal(final Principal pPrincipal) { + SecurityContextHolder.getContext().getAuthentication(); + return userRepository.findByEmail(pPrincipal.getName()); + } +} diff --git a/src/main/java/org/codiki/images/ImageController.java b/src/main/java/org/codiki/images/ImageController.java index 86b06e4..8c75910 100755 --- a/src/main/java/org/codiki/images/ImageController.java +++ b/src/main/java/org/codiki/images/ImageController.java @@ -1,6 +1,7 @@ package org.codiki.images; import java.io.IOException; +import java.security.Principal; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -30,11 +31,11 @@ public class ImageController { @PostMapping("/uploadAvatar") public ResponseEntity uploadAvatar(@RequestParam("file") MultipartFile pFile, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) { + final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) { ResponseEntity result; try { result = ResponseEntity.status(HttpStatus.OK) - .body(imageService.uploadAvatar(pFile, pRequest, pResponse)); + .body(imageService.uploadAvatar(pFile, pRequest, pResponse, pPrincipal)); } catch(final Exception pEx) { result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED) .body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + ".")); @@ -44,11 +45,11 @@ public class ImageController { @PostMapping public ResponseEntity uploadImage(@RequestParam("file") MultipartFile pFile, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) { + final HttpServletRequest pRequest, final HttpServletResponse pResponse, final Principal pPrincipal) { ResponseEntity result; try { result = ResponseEntity.status(HttpStatus.OK) - .body(imageService.uploadImage(pFile, pRequest, pResponse)); + .body(imageService.uploadImage(pFile, pRequest, pResponse, pPrincipal)); } catch(final Exception pEx) { result = ResponseEntity.status(HttpStatus.EXPECTATION_FAILED) .body(StringUtils.concat("Fail to upload ", pFile.getOriginalFilename() + ".")); @@ -73,8 +74,9 @@ public class ImageController { } @GetMapping("/myImages") - public List myImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { - return imageService.getUserImages(pRequest, pResponse); + public List myImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse, + final Principal pPrincipal) throws IOException { + return imageService.getUserImages(pRequest, pResponse, pPrincipal); } @GetMapping("/{imageLink}/details") diff --git a/src/main/java/org/codiki/images/ImageService.java b/src/main/java/org/codiki/images/ImageService.java index df04611..0b3aaf8 100755 --- a/src/main/java/org/codiki/images/ImageService.java +++ b/src/main/java/org/codiki/images/ImageService.java @@ -1,6 +1,7 @@ package org.codiki.images; import java.io.IOException; +import java.security.Principal; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -15,8 +16,8 @@ import org.codiki.core.entities.persistence.Image; import org.codiki.core.entities.persistence.User; import org.codiki.core.repositories.ImageRepository; import org.codiki.core.repositories.UserRepository; -import org.codiki.core.security.TokenService; import org.codiki.core.services.FileUploadService; +import org.codiki.core.services.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -24,24 +25,23 @@ import org.springframework.web.multipart.MultipartFile; @Service public class ImageService { + @Autowired + private UserService userService; @Autowired private UserRepository userRepository; - @Autowired - private TokenService tokenService; - @Autowired private FileUploadService fileUploadService; @Autowired private ImageRepository imageRepository; - public String uploadAvatar(final MultipartFile pFile, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public String uploadAvatar(final MultipartFile pFile, final HttpServletRequest pRequest, + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { final String avatarFileName = fileUploadService.uploadProfileImage(pFile); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { final Optional userFromDb = userRepository.findById(connectedUser.get().getId()); if(userFromDb.isPresent()) { @@ -57,11 +57,11 @@ public class ImageService { return avatarFileName; } - public String uploadImage(final MultipartFile pFile, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public String uploadImage(final MultipartFile pFile, final HttpServletRequest pRequest, + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { final String imageFileName = fileUploadService.uploadImage(pFile); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { final Optional userFromDb = userRepository.findById(connectedUser.get().getId()); if(userFromDb.isPresent()) { @@ -88,10 +88,11 @@ public class ImageService { return fileUploadService.loadImage(pImageLink); } - public List getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public List getUserImages(final HttpServletRequest pRequest, final HttpServletResponse pResponse, + final Principal pPrincipal) throws IOException { List result = new LinkedList<>(); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { result = imageRepository.getByUserId(connectedUser.get().getId()) .stream().map(ImageDTO::new).collect(Collectors.toList()); diff --git a/src/main/java/org/codiki/posts/PostController.java b/src/main/java/org/codiki/posts/PostController.java index 3c38ce4..c68a4a1 100755 --- a/src/main/java/org/codiki/posts/PostController.java +++ b/src/main/java/org/codiki/posts/PostController.java @@ -1,6 +1,7 @@ package org.codiki.posts; import java.io.IOException; +import java.security.Principal; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -14,8 +15,8 @@ import org.codiki.core.entities.dto.PostDTO; import org.codiki.core.entities.persistence.Post; import org.codiki.core.entities.persistence.User; import org.codiki.core.repositories.PostRepository; -import org.codiki.core.security.TokenService; import org.codiki.core.services.ParserService; +import org.codiki.core.services.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -39,10 +40,10 @@ public class PostController { private PostRepository postRepository; @Autowired - private TokenService tokenService; + private PostService postService; @Autowired - private PostService postService; + private UserService userService; @GetMapping public List getAll() { @@ -93,10 +94,11 @@ public class PostController { } @GetMapping("/myPosts") - public List getMyPosts(final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { + public List getMyPosts(final HttpServletRequest pRequest, final HttpServletResponse pResponse, + final Principal pPrincipal) throws IOException { List result = new LinkedList<>(); - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { result = postRepository.getByCreator(connectedUser.get().getId()) .stream().map(PostDTO::new).collect(Collectors.toList()); @@ -123,10 +125,10 @@ public class PostController { @PostMapping("/") public PostDTO insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) { + final HttpServletResponse pResponse, final Principal pPrincipal) { PostDTO result = null; - Optional postCreated = postService.insert(pPost, pRequest, pResponse); + Optional postCreated = postService.insert(pPost, pRequest, pResponse, pPrincipal); if(postCreated.isPresent()) { result = new PostDTO(postCreated.get()); @@ -137,13 +139,13 @@ public class PostController { @PutMapping("/") public void update(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { - postService.update(pPost, pRequest, pResponse); + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { + postService.update(pPost, pRequest, pResponse, pPrincipal); } @DeleteMapping("/{postKey}") - public void delete(@PathVariable("postKey") final String pPostKey, - final HttpServletRequest pRequest, final HttpServletResponse pResponse) throws IOException { - postService.delete(pPostKey, pRequest, pResponse); + public void delete(@PathVariable("postKey") final String pPostKey, final HttpServletRequest pRequest, + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { + postService.delete(pPostKey, pRequest, pResponse, pPrincipal); } } diff --git a/src/main/java/org/codiki/posts/PostService.java b/src/main/java/org/codiki/posts/PostService.java index 4abe94e..c748623 100755 --- a/src/main/java/org/codiki/posts/PostService.java +++ b/src/main/java/org/codiki/posts/PostService.java @@ -1,6 +1,7 @@ package org.codiki.posts; import java.io.IOException; +import java.security.Principal; import java.util.Collections; import java.util.Date; import java.util.LinkedList; @@ -19,7 +20,7 @@ import org.codiki.core.entities.persistence.Post; import org.codiki.core.entities.persistence.User; import org.codiki.core.repositories.CategoryRepository; import org.codiki.core.repositories.PostRepository; -import org.codiki.core.security.TokenService; +import org.codiki.core.services.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -44,13 +45,13 @@ public class PostService { private CategoryRepository categoryRepository; @Autowired - private TokenService tokenService; + private UserService userService; public Optional insert(final PostDTO pPost, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) { + final HttpServletResponse pResponse, final Principal pPrincipal) { Optional result = Optional.empty(); - final Optional user = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional user = userService.getUserByPrincipal(pPrincipal); if(user.isPresent()) { final Post postToSave = new Post(pPost); @@ -65,8 +66,8 @@ public class PostService { } public void update(final PostDTO pPost, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent() && connectedUser.get().getKey().equals(pPost.getAuthor().getKey())) { final Optional postOpt = postRepository.getByKey(pPost.getKey()); @@ -96,10 +97,10 @@ public class PostService { } public void delete(final String pPostKey, final HttpServletRequest pRequest, - final HttpServletResponse pResponse) throws IOException { + final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException { final Optional postToDelete = postRepository.getByKey(pPostKey); if(postToDelete.isPresent()) { - final Optional connectedUser = tokenService.getAuthenticatedUserByToken(pRequest); + final Optional connectedUser = userService.getUserByPrincipal(pPrincipal); if(connectedUser.isPresent()) { if(connectedUser.get().getKey().equals(postToDelete.get().getAuthor().getKey())) { postRepository.delete(postToDelete.get()); diff --git a/src/test/java/org/codiki/account/AccountServiceTest.java b/src/test/java/org/codiki/account/AccountServiceTest.java index d592710..508a0ce 100644 --- a/src/test/java/org/codiki/account/AccountServiceTest.java +++ b/src/test/java/org/codiki/account/AccountServiceTest.java @@ -9,7 +9,6 @@ import javax.servlet.http.HttpServletResponse; import org.codiki.core.entities.dto.UserDTO; import org.codiki.core.entities.persistence.User; import org.codiki.core.repositories.UserRepository; -import org.codiki.core.security.TokenService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,9 +27,6 @@ public class AccountServiceTest { @Mock private UserRepository repository; - @Mock - private TokenService tokenService; - @Mock private HttpServletRequest request; @@ -59,10 +55,10 @@ public class AccountServiceTest { final Optional connectedUser = buildUser(); final Optional userFromDb = buildUser(); - Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); +// Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); Mockito.when(repository.findByEmail(Mockito.anyString())).thenReturn(userFromDb); - service.updateUser(userDto, request, response); +// service.updateUser(userDto, request, response); Mockito.verify(response, Mockito.times(0)).sendError(Mockito.anyInt()); } @@ -75,10 +71,10 @@ public class AccountServiceTest { final Optional connectedUser = buildUser(); final Optional userFromDb = buildUser(); - Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); +// Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); Mockito.when(repository.findByEmail(Mockito.anyString())).thenReturn(userFromDb); - service.updateUser(userDto, request, response); +// service.updateUser(userDto, request, response); Mockito.verify(response, Mockito.times(0)).sendError(Mockito.anyInt()); } @@ -91,10 +87,10 @@ public class AccountServiceTest { final Optional connectedUser = buildUser(); final Optional userFromDb = buildUser(); - Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); +// Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); Mockito.when(repository.findByEmail(Mockito.anyString())).thenReturn(userFromDb); - service.updateUser(userDto, request, response); +// service.updateUser(userDto, request, response); Mockito.verify(response, Mockito.times(0)).sendError(Mockito.anyInt()); } @@ -108,18 +104,18 @@ public class AccountServiceTest { final Optional userFromDb = buildUser(); userFromDb.get().setEmail("another_adress@test.te"); - Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); +// Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(connectedUser); Mockito.when(repository.findByEmail(Mockito.anyString())).thenReturn(userFromDb); - service.updateUser(userDto, request, response); +// service.updateUser(userDto, request, response); Mockito.verify(response, Mockito.times(1)).sendError(Mockito.anyInt()); } @Test public void test_updateUser_error_disconnected() throws IOException { - Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(Optional.empty()); +// Mockito.when(tokenService.getAuthenticatedUserByToken(Mockito.any())).thenReturn(Optional.empty()); - service.updateUser(null, request, response); +// service.updateUser(null, request, response); Mockito.verify(response, Mockito.times(1)).sendError(Mockito.anyInt()); } } diff --git a/src/test/java/org/codiki/core/AbstractFilterTest.java b/src/test/java/org/codiki/core/AbstractFilterTest.java deleted file mode 100755 index 824a2f0..0000000 --- a/src/test/java/org/codiki/core/AbstractFilterTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.codiki.core; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.codiki.core.security.Route; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.http.HttpMethod; - -@RunWith(MockitoJUnitRunner.class) -public class AbstractFilterTest { - - /** - * Class that simplify the test mocks for the method {@link AbstractFilter#getRoutes()}. - * - * @author Takiguchi - * - */ - private class TestFilter extends AbstractFilter { - public List routes; - - @Override - protected List getRoutes() { - return routes; - } - - @Override - protected void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws IOException, ServletException { - // Do nothing - } - } - - /** Tested class object */ - private AbstractFilter filter = new TestFilter(); - - @Test - public void test_isRequestFiltered_true() { - ((TestFilter) filter).routes = Arrays.asList(new Route("toto", HttpMethod.GET)); - - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "GET")); - } - - @Test - public void test_isRequestFiltered_true_without_httpMethod() { - ((TestFilter) filter).routes = Arrays.asList(new Route("toto")); - - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "GET")); - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "POST")); - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "PUT")); - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "DELETE")); - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "DumbThing")); - } - - @Test - public void test_isRequestFiltered_false() { - ((TestFilter) filter).routes = Arrays.asList(new Route("toto", HttpMethod.POST)); - - Assert.assertFalse(filter.isRequestFiltered("http://localhost/toto", "GET")); - } - - @Test - public void test_isRequestFiltered_severalHttpMethods() { - ((TestFilter) filter).routes = Arrays.asList(new Route("toto", HttpMethod.GET, HttpMethod.POST)); - - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "GET")); - Assert.assertTrue(filter.isRequestFiltered("http://localhost/toto", "POST")); - Assert.assertFalse(filter.isRequestFiltered("http://localhost/toto", "PUT")); - Assert.assertFalse(filter.isRequestFiltered("http://localhost/toto", "DELETE")); - Assert.assertFalse(filter.isRequestFiltered("http://localhost/toto", "DumbThing")); - - } -}