diff --git a/pom.xml b/pom.xml index 5bf5af7..1d08541 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,13 @@ spring-boot-starter-web + + + com.auth0 + java-jwt + 3.5.0 + + org.projectlombok lombok diff --git a/src/main/java/me/aski/catalogueserviceauth/CatalogueServiceAuthApplication.java b/src/main/java/me/aski/catalogueserviceauth/CatalogueServiceAuthApplication.java index ceaf0f0..28b3ecb 100644 --- a/src/main/java/me/aski/catalogueserviceauth/CatalogueServiceAuthApplication.java +++ b/src/main/java/me/aski/catalogueserviceauth/CatalogueServiceAuthApplication.java @@ -1,7 +1,15 @@ package me.aski.catalogueserviceauth; +import me.aski.catalogueserviceauth.dao.RoleRepository; +import me.aski.catalogueserviceauth.dao.UserRepository; +import me.aski.catalogueserviceauth.service.AccountService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.stream.Stream; @SpringBootApplication public class CatalogueServiceAuthApplication { @@ -10,5 +18,29 @@ public class CatalogueServiceAuthApplication { SpringApplication.run(CatalogueServiceAuthApplication.class, args); } + @Bean + CommandLineRunner start(AccountService accountService, UserRepository userRepository, RoleRepository roleRepository) { + return args -> { + + accountService.clearDB(); + //userRepository.save(new User(null,"Abdellahaski","111111",true,null)); + accountService.saveRole("ADMIN"); + accountService.saveRole("USER"); + Stream.of("AbdellahASKI", "user1", "user2", "user3").forEach(username -> { + accountService.saveUser(username, "1234", "1234"); + }); + accountService.addRoleToUser("AbdellahASKI", "ADMIN"); + + System.out.println(accountService.loadUserByUsername("AbdellahASKI")); + + + }; + } + + @Bean + BCryptPasswordEncoder getBCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + } diff --git a/src/main/java/me/aski/catalogueserviceauth/dao/RoleRepository.java b/src/main/java/me/aski/catalogueserviceauth/dao/RoleRepository.java new file mode 100644 index 0000000..790031c --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/dao/RoleRepository.java @@ -0,0 +1,10 @@ +package me.aski.catalogueserviceauth.dao; + +import me.aski.catalogueserviceauth.entities.Role; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface RoleRepository extends MongoRepository { + public Role findByRoleName(String roleName); +} diff --git a/src/main/java/me/aski/catalogueserviceauth/dao/UserRepository.java b/src/main/java/me/aski/catalogueserviceauth/dao/UserRepository.java new file mode 100644 index 0000000..3c3a3c7 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/dao/UserRepository.java @@ -0,0 +1,11 @@ +package me.aski.catalogueserviceauth.dao; + +import me.aski.catalogueserviceauth.entities.User; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource +public interface UserRepository extends MongoRepository { + + public User findByUsername(String username); +} diff --git a/src/main/java/me/aski/catalogueserviceauth/entities/Role.java b/src/main/java/me/aski/catalogueserviceauth/entities/Role.java new file mode 100644 index 0000000..f6c83ab --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/entities/Role.java @@ -0,0 +1,25 @@ +package me.aski.catalogueserviceauth.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Role { + @Id + private String id; + private String roleName; + + @Override + public String toString() { + return "Role{" + + "id='" + id + '\'' + + ", roleName='" + roleName + '\'' + + '}'; + } +} diff --git a/src/main/java/me/aski/catalogueserviceauth/entities/User.java b/src/main/java/me/aski/catalogueserviceauth/entities/User.java new file mode 100644 index 0000000..9e5566c --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/entities/User.java @@ -0,0 +1,29 @@ +package me.aski.catalogueserviceauth.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.ArrayList; +import java.util.Collection; + +@Document +@Data +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class User { + private String id; + @Indexed(unique = true) + private String username; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + private boolean active; + @DBRef + private Collection roles = new ArrayList<>(); +} diff --git a/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthentificationFilter.java b/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthentificationFilter.java new file mode 100644 index 0000000..fe58894 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthentificationFilter.java @@ -0,0 +1,58 @@ +package me.aski.catalogueserviceauth.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.fasterxml.jackson.databind.ObjectMapper; +import me.aski.catalogueserviceauth.entities.User; +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.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class JWTAuthentificationFilter extends UsernamePasswordAuthenticationFilter { + private AuthenticationManager authenticationManager; + + public JWTAuthentificationFilter(AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + try { + User user = new ObjectMapper().readValue(request.getInputStream(), User.class); + return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Invalid Request: " + e); + } + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, + Authentication authResult) throws IOException, ServletException { + org.springframework.security.core.userdetails.User user = + (org.springframework.security.core.userdetails.User) authResult.getPrincipal(); + List roles = new ArrayList<>(); + authResult.getAuthorities().forEach(role -> { + roles.add(role.getAuthority()); + }); + String jwt = JWT.create() + .withIssuer(request.getRequestURI()) + .withSubject(user.getUsername()) + .withArrayClaim("roles", roles.toArray(new String[0])) + .withExpiresAt(new Date(System.currentTimeMillis() + SecurityParams.EXPIRATION)) + .sign(Algorithm.HMAC256(SecurityParams.SECRET)); + System.out.println(jwt); + response.addHeader(SecurityParams.HEADER_NAME, SecurityParams.PREFIX + jwt); + } +} diff --git a/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthorizationFilter.java b/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthorizationFilter.java new file mode 100644 index 0000000..02e83f7 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/security/JWTAuthorizationFilter.java @@ -0,0 +1,62 @@ +package me.aski.catalogueserviceauth.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public class JWTAuthorizationFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT"); + String allHeaders = "No-Auth, Accept, Accept-CH, Accept-Charset, Accept-Datetime, Accept-Encoding, Accept-Ext, Accept-Features, Accept-Language, Accept-Params, Accept-Ranges, Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Access-Control-Expose-Headers, Access-Control-Max-Age, Access-Control-Request-Headers, Access-Control-Request-Method, Age, Allow, Alternates, Authentication-Info, Authorization, C-Ext, C-Man, C-Opt, C-PEP, C-PEP-Info, CONNECT, Cache-Control, Compliance, Connection, Content-Base, Content-Disposition, Content-Encoding, Content-ID, Content-Language, Content-Length, Content-Location, Content-MD5, Content-Range, Content-Script-Type, Content-Security-Policy, Content-Style-Type, Content-Transfer-Encoding, Content-Type, Content-Version, Cookie, Cost, DAV, DELETE, DNT, DPR, Date, Default-Style, Delta-Base, Depth, Derived-From, Destination, Differential-ID, Digest, ETag, Expect, Expires, Ext, From, GET, GetProfile, HEAD, HTTP-date, Host, IM, If, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Keep-Alive, Label, Last-Event-ID, Last-Modified, Link, Location, Lock-Token, MIME-Version, Man, Max-Forwards, Media-Range, Message-ID, Meter, Negotiate, Non-Compliance, OPTION, OPTIONS, OWS, Opt, Optional, Ordering-Type, Origin, Overwrite, P3P, PEP, PICS-Label, POST, PUT, Pep-Info, Permanent, Position, Pragma, ProfileObject, Protocol, Protocol-Query, Protocol-Request, Proxy-Authenticate, Proxy-Authentication-Info, Proxy-Authorization, Proxy-Features, Proxy-Instruction, Public, RWS, Range, Referer, Refresh, Resolution-Hint, Resolver-Location, Retry-After, Safe, Sec-Websocket-Extensions, Sec-Websocket-Key, Sec-Websocket-Origin, Sec-Websocket-Protocol, Sec-Websocket-Version, Security-Scheme, Server, Set-Cookie, Set-Cookie2, SetProfile, SoapAction, Status, Status-URI, Strict-Transport-Security, SubOK, Subst, Surrogate-Capability, Surrogate-Control, TCN, TE, TRACE, Timeout, Title, Trailer, Transfer-Encoding, UA-Color, UA-Media, UA-Pixels, UA-Resolution, UA-Windowpixels, URI, Upgrade, User-Agent, Variant-Vary, Vary, Version, Via, Viewport-Width, WWW-Authenticate, Want-Digest, Warning, Width, X-Content-Duration, X-Content-Security-Policy, X-Content-Type-Options, X-CustomHeader, X-DNSPrefetch-Control, X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto, X-Frame-Options, X-Modified, X-OTHER, X-PING, X-PINGOTHER, X-Powered-By, X-Requested-With"; + response.setHeader("Access-Control-Allow-Headers", allHeaders); + response.setHeader("Access-Control-Allow-Credentials", "true"); + + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + response.setStatus(HttpServletResponse.SC_OK); + } else if (request.getRequestURI().equalsIgnoreCase("/login")) { + filterChain.doFilter(request, response); + return; + } else { + + String jwtToken = request.getHeader(SecurityParams.HEADER_NAME); + if (jwtToken == null || !jwtToken.startsWith(SecurityParams.PREFIX)) { + //throw new RuntimeException("Not Authorized"); + filterChain.doFilter(request, response); + return; + } + + String jwt = jwtToken.substring(SecurityParams.PREFIX.length()); + + JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SecurityParams.SECRET)) + .build(); + DecodedJWT decodedJWT = verifier.verify(jwt); + String username = decodedJWT.getSubject(); + List roles = decodedJWT.getClaims().get("roles").asList(String.class); + + Collection authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + + UsernamePasswordAuthenticationToken user = new UsernamePasswordAuthenticationToken(username, null, authorities); + SecurityContextHolder.getContext().setAuthentication(user); + + filterChain.doFilter(request, response); + } + } +} diff --git a/src/main/java/me/aski/catalogueserviceauth/security/SecurityConfig.java b/src/main/java/me/aski/catalogueserviceauth/security/SecurityConfig.java new file mode 100644 index 0000000..6ff0b77 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/security/SecurityConfig.java @@ -0,0 +1,46 @@ +package me.aski.catalogueserviceauth.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +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.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + private final UserDetailsService userDetailsService; + private final BCryptPasswordEncoder passwordEncoder; + + @Autowired + public SecurityConfig(@Qualifier("userDetailsServiceImpl") UserDetailsService userDetailsService, BCryptPasswordEncoder passwordEncoder) { + this.userDetailsService = userDetailsService; + this.passwordEncoder = passwordEncoder; + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests().antMatchers("/login/**", "/register/**").permitAll() + .and() + .authorizeRequests().antMatchers("/users/**", "/roles/**").hasAuthority("ADMIN") + .anyRequest().authenticated() + .and() + .addFilter(new JWTAuthentificationFilter(authenticationManager())) + .addFilterBefore(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); + + } +} diff --git a/src/main/java/me/aski/catalogueserviceauth/security/SecurityParams.java b/src/main/java/me/aski/catalogueserviceauth/security/SecurityParams.java new file mode 100644 index 0000000..94acaf6 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/security/SecurityParams.java @@ -0,0 +1,10 @@ +package me.aski.catalogueserviceauth.security; + +public interface SecurityParams { + public final static String HEADER_NAME = "Authorization"; + public final static String SECRET = "abdellah@aski.me"; + public final static long EXPIRATION = 10 * 24 * 3600; + public final static String PREFIX = "Bearer "; + + +} diff --git a/src/main/java/me/aski/catalogueserviceauth/security/UserDetailsServiceImpl.java b/src/main/java/me/aski/catalogueserviceauth/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..c79086c --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/security/UserDetailsServiceImpl.java @@ -0,0 +1,33 @@ +package me.aski.catalogueserviceauth.security; + +import me.aski.catalogueserviceauth.entities.User; +import me.aski.catalogueserviceauth.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.stream.Collectors; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private final AccountService accountService; + + @Autowired + public UserDetailsServiceImpl(AccountService accountService) { + this.accountService = accountService; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = accountService.loadUserByUsername(username); + if (user == null) throw new UsernameNotFoundException("Invalid User"); + Collection authorities = user.getRoles().stream().map(role -> new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList()); + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); + } +} diff --git a/src/main/java/me/aski/catalogueserviceauth/service/AccountService.java b/src/main/java/me/aski/catalogueserviceauth/service/AccountService.java new file mode 100644 index 0000000..5f02c4c --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/service/AccountService.java @@ -0,0 +1,16 @@ +package me.aski.catalogueserviceauth.service; + +import me.aski.catalogueserviceauth.entities.Role; +import me.aski.catalogueserviceauth.entities.User; + +public interface AccountService { + User saveUser(String username, String password, String rePassword); + + Role saveRole(String roleName); + + User loadUserByUsername(String username); + + User addRoleToUser(String username, String role); + + void clearDB(); +} diff --git a/src/main/java/me/aski/catalogueserviceauth/service/AccountServiceImpl.java b/src/main/java/me/aski/catalogueserviceauth/service/AccountServiceImpl.java new file mode 100644 index 0000000..97f0d5f --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/service/AccountServiceImpl.java @@ -0,0 +1,69 @@ +package me.aski.catalogueserviceauth.service; + +import me.aski.catalogueserviceauth.dao.RoleRepository; +import me.aski.catalogueserviceauth.dao.UserRepository; +import me.aski.catalogueserviceauth.entities.Role; +import me.aski.catalogueserviceauth.entities.User; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; + +@Service +@Transactional +public class AccountServiceImpl implements AccountService { + + + private final RoleRepository roleRepository; + private final UserRepository userRepository; + private BCryptPasswordEncoder passwordEncoder; + + //@Autowired + public AccountServiceImpl(RoleRepository roleRepository, UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) { + this.roleRepository = roleRepository; + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public User saveUser(String username, String password, String rePassword) { + username = username.toLowerCase(); + if (userRepository.findByUsername(username) != null) throw new RuntimeException("Username already exist"); + if (rePassword == null || rePassword.isEmpty() || !password.equals(rePassword)) + throw new RuntimeException("Please confirm your password"); + + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setActive(true); + user.setRoles(new ArrayList<>()); + userRepository.save(user); + + return addRoleToUser(username, "USER"); + } + + @Override + public Role saveRole(String roleName) { + return roleRepository.save(new Role(null, roleName)); + } + + @Override + public User loadUserByUsername(String username) { + return userRepository.findByUsername(username.toLowerCase()); + } + + @Override + public User addRoleToUser(String username, String role) { + User user = loadUserByUsername(username); + user.getRoles().add(roleRepository.findByRoleName(role)); + return userRepository.save(user); + } + + @Override + public void clearDB() { + userRepository.deleteAll(); + roleRepository.deleteAll(); + } + +} diff --git a/src/main/java/me/aski/catalogueserviceauth/web/UserController.java b/src/main/java/me/aski/catalogueserviceauth/web/UserController.java new file mode 100644 index 0000000..4a8d511 --- /dev/null +++ b/src/main/java/me/aski/catalogueserviceauth/web/UserController.java @@ -0,0 +1,36 @@ +package me.aski.catalogueserviceauth.web; + +import lombok.Data; +import lombok.ToString; +import me.aski.catalogueserviceauth.entities.User; +import me.aski.catalogueserviceauth.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class UserController { + + private final AccountService accountService; + + @Autowired + public UserController(AccountService accountService) { + this.accountService = accountService; + } + + @PostMapping("/register") + public User register(@RequestBody UserForm userForm) { + System.out.println(userForm.toString()); + return accountService.saveUser(userForm.getUsername(), userForm.getPassword(), userForm.getRePassword()); + } +} + +@Data +@ToString +class UserForm { + private String username; + private String password; + private String rePassword; + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e69de29..56198c3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.data.mongodb.uri=mongodb://localhost:27017/CatalogueService \ No newline at end of file