diff --git a/build.gradle b/build.gradle index 43f86453..4b0daa39 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,10 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '2.1.3.RELEASE' + + compile group: 'org.springframework.boot', name: 'spring-boot-starter-security' + compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.3.5.RELEASE' + compile group: 'commons-dbcp', name: 'commons-dbcp', version: '1.2.2' + } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 44e7c4d1..3189028a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Nov 08 15:41:57 EAT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/com/coyoapp/tinytask/ResourceConstants.java b/src/main/java/com/coyoapp/tinytask/ResourceConstants.java index 61eb4ba3..ce378cd5 100644 --- a/src/main/java/com/coyoapp/tinytask/ResourceConstants.java +++ b/src/main/java/com/coyoapp/tinytask/ResourceConstants.java @@ -1,8 +1,6 @@ package com.coyoapp.tinytask; public class ResourceConstants { - public static final String TINY_TASKS_V1 = "/v1/tiny-tasks/"; - //Everyday at 8:00 AM public static final String CRON_EVERYDAY_EXPRESSION = "0 0 8 * * *"; @@ -10,4 +8,7 @@ public class ResourceConstants { public static final String CRON_EVERY_TWO_SECONDS_EXPRESSION = "2 * * * * *"; public static final String EMAIL_SUBJECT = "Unfinished Tiny Tasks"; + + public static final String TINY_TASKS_V1_UNSECURED = "/v1/tiny-tasks/"; + public static final String TINY_TASKS_V1_SECURED = "/v1/tiny-tasks/secured/"; } diff --git a/src/main/java/com/coyoapp/tinytask/auth/CustomUserDetails.java b/src/main/java/com/coyoapp/tinytask/auth/CustomUserDetails.java new file mode 100644 index 00000000..4b476ff0 --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/auth/CustomUserDetails.java @@ -0,0 +1,63 @@ +package com.coyoapp.tinytask.auth; + +import com.coyoapp.tinytask.domain.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class CustomUserDetails implements UserDetails { + + private User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + if(null == user.getRoles()) { + return Collections.emptySet(); + } + Set grantedAuthorities = new HashSet<>(); + user.getRoles().forEach(role -> { + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRole().toUpperCase())); + }); + + return grantedAuthorities; + } + + @Override + public String getPassword() { + return this.user.getPassword(); + } + + @Override + public String getUsername() { + return this.user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/coyoapp/tinytask/batch/SendEmailBatch.java b/src/main/java/com/coyoapp/tinytask/batch/SendEmailBatch.java index e4b3fc6b..549939c1 100644 --- a/src/main/java/com/coyoapp/tinytask/batch/SendEmailBatch.java +++ b/src/main/java/com/coyoapp/tinytask/batch/SendEmailBatch.java @@ -33,7 +33,7 @@ public class SendEmailBatch { - @Scheduled(cron = ResourceConstants.CRON_EVERYDAY_EXPRESSION) + @Scheduled(cron = ResourceConstants.CRON_EVERY_TWO_SECONDS_EXPRESSION) public void sendUsersEmail() { logger.info("Email Schedule started"); diff --git a/src/main/java/com/coyoapp/tinytask/configuration/AuthServerConfig.java b/src/main/java/com/coyoapp/tinytask/configuration/AuthServerConfig.java new file mode 100644 index 00000000..1ff0099c --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/configuration/AuthServerConfig.java @@ -0,0 +1,53 @@ +package com.coyoapp.tinytask.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.provider.token.TokenStore; + +@Configuration +@EnableAuthorizationServer +public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { + + static final String CLIEN_ID = "tiny-task-client"; + static final String CLIENT_SECRET = "tiny-secret"; + static final String GRANT_TYPE_PASSWORD = "password"; + static final String AUTHORIZATION_CODE = "authorization_code"; + static final String REFRESH_TOKEN = "refresh_token"; + static final String IMPLICIT = "implicit"; + static final String SCOPE_READ = "read"; + static final String SCOPE_WRITE = "write"; + static final String TRUST = "trust"; + static final int ACCESS_TOKEN_VALIDITY_SECONDS = 1 * 60 * 60; + static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 6 * 60 * 60; + + @Autowired + private TokenStore tokenStore; + + @Autowired + private AuthenticationManager authenticationManager; + + @Override + public void configure(ClientDetailsServiceConfigurer configurer) throws Exception { + + configurer + .inMemory() + .withClient(CLIEN_ID) + .secret(new BCryptPasswordEncoder().encode(CLIENT_SECRET)) + .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT) + .scopes(SCOPE_READ, SCOPE_WRITE, TRUST) + .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS). + refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS); + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + endpoints.tokenStore(tokenStore) + .authenticationManager(authenticationManager); + } +} diff --git a/src/main/java/com/coyoapp/tinytask/configuration/OAuthCoresConfig.java b/src/main/java/com/coyoapp/tinytask/configuration/OAuthCoresConfig.java new file mode 100644 index 00000000..3e72601f --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/configuration/OAuthCoresConfig.java @@ -0,0 +1,30 @@ +package com.coyoapp.tinytask.configuration; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class OAuthCoresConfig { + @Bean + public FilterRegistrationBean customCorsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("http://localhost:4200"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/oauth/token", config); + FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); + + //IMPORTANT #2: I didn't stress enough the importance of this line in my original answer, + //but it's here where we tell Spring to load this filter at the right point in the chain + //(with an order of precedence higher than oauth2's filters) + bean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return bean; + } +} diff --git a/src/main/java/com/coyoapp/tinytask/configuration/ResourceServerConfig.java b/src/main/java/com/coyoapp/tinytask/configuration/ResourceServerConfig.java new file mode 100644 index 00000000..c88fa241 --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/configuration/ResourceServerConfig.java @@ -0,0 +1,29 @@ +package com.coyoapp.tinytask.configuration; + +import com.coyoapp.tinytask.ResourceConstants; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler; + +@Configuration +@EnableResourceServer +public class ResourceServerConfig extends ResourceServerConfigurerAdapter { + private static final String RESOURCE_ID = "resource_id"; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources.resourceId(RESOURCE_ID).stateless(false); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http.cors().and() + .anonymous().disable() + .authorizeRequests() + .antMatchers(ResourceConstants.TINY_TASKS_V1_SECURED + "**").access( "hasRole('ROLE_USER')") + .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler()); + } +} diff --git a/src/main/java/com/coyoapp/tinytask/configuration/SecurityConfig.java b/src/main/java/com/coyoapp/tinytask/configuration/SecurityConfig.java new file mode 100644 index 00000000..b6f7c386 --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/configuration/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.coyoapp.tinytask.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +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.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; + +import javax.annotation.Resource; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Resource(name = "userService") + private UserDetailsService userDetailsService; + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Autowired + public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService) + .passwordEncoder(encoder()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .csrf().disable() + .anonymous().disable() + .authorizeRequests() + .antMatchers("/**").permitAll(); + } + + @Bean + public TokenStore tokenStore() { + return new InMemoryTokenStore(); + } + + @Bean + public BCryptPasswordEncoder encoder(){ + return new BCryptPasswordEncoder(); + } + +} diff --git a/src/main/java/com/coyoapp/tinytask/service/impl/UserServiceImpl.java b/src/main/java/com/coyoapp/tinytask/service/impl/UserServiceImpl.java index d5415b45..2ed7d93f 100644 --- a/src/main/java/com/coyoapp/tinytask/service/impl/UserServiceImpl.java +++ b/src/main/java/com/coyoapp/tinytask/service/impl/UserServiceImpl.java @@ -1,6 +1,7 @@ package com.coyoapp.tinytask.service.impl; +import com.coyoapp.tinytask.auth.CustomUserDetails; import com.coyoapp.tinytask.domain.Role; import com.coyoapp.tinytask.domain.User; import com.coyoapp.tinytask.dto.UserRequest; @@ -11,13 +12,16 @@ import com.coyoapp.tinytask.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; +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.ArrayList; import java.util.List; @Service(value = "userService") -public class UserServiceImpl implements UserService { +public class UserServiceImpl implements UserDetailsService, UserService { @Autowired private UserRepository userRepository; @@ -28,6 +32,14 @@ public class UserServiceImpl implements UserService { @Autowired ConversionService conversionService; + public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { + User user = userRepository.findByUsername(userId); + if(user == null){ + throw new UsernameNotFoundException("Invalid username or password."); + } + return new CustomUserDetails(user); + } + @Override public List getAllUsers() { List users = (List) userRepository.findAll(); diff --git a/src/main/java/com/coyoapp/tinytask/web/TaskController.java b/src/main/java/com/coyoapp/tinytask/web/TaskController.java index 5b9d42b6..5ccb25ed 100644 --- a/src/main/java/com/coyoapp/tinytask/web/TaskController.java +++ b/src/main/java/com/coyoapp/tinytask/web/TaskController.java @@ -18,7 +18,7 @@ @Slf4j @RestController -@RequestMapping(ResourceConstants.TINY_TASKS_V1 + "tasks") +@RequestMapping(ResourceConstants.TINY_TASKS_V1_SECURED + "tasks") @RequiredArgsConstructor public class TaskController { diff --git a/src/main/java/com/coyoapp/tinytask/web/TinyTaskController.java b/src/main/java/com/coyoapp/tinytask/web/TinyTaskController.java new file mode 100644 index 00000000..68574a3b --- /dev/null +++ b/src/main/java/com/coyoapp/tinytask/web/TinyTaskController.java @@ -0,0 +1,49 @@ +package com.coyoapp.tinytask.web; + +import com.coyoapp.tinytask.ResourceConstants; +import com.coyoapp.tinytask.domain.User; +import com.coyoapp.tinytask.dto.TaskResponse; +import com.coyoapp.tinytask.repository.UserRepository; +import com.coyoapp.tinytask.service.TinyTaskService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping(ResourceConstants.TINY_TASKS_V1_SECURED) +@RequiredArgsConstructor +public class TinyTaskController { + + @Autowired + private UserRepository userRepository; + + private final TinyTaskService tinyTaskService; + + @GetMapping(path = "user/{userId}/tasks",produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity> getAllUserTasks(@PathVariable String userId) { + return new ResponseEntity<>(tinyTaskService.getAllUserTasks(userId), HttpStatus.OK); + } + + @GetMapping(path = "current-user",produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity getLoggedInUser() { + UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + String username = principal.getUsername(); + + User user = userRepository.findByUsername(username); + + return new ResponseEntity<>(user, HttpStatus.OK); + } +} diff --git a/src/main/java/com/coyoapp/tinytask/web/UserController.java b/src/main/java/com/coyoapp/tinytask/web/UserController.java index 7f8734e5..2355f888 100644 --- a/src/main/java/com/coyoapp/tinytask/web/UserController.java +++ b/src/main/java/com/coyoapp/tinytask/web/UserController.java @@ -17,34 +17,33 @@ @Slf4j @RestController @RequiredArgsConstructor -@RequestMapping(ResourceConstants.TINY_TASKS_V1 + "users") public class UserController { private final UserService userService; - @GetMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(path = ResourceConstants.TINY_TASKS_V1_SECURED + "users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity> getAllUsers() { return new ResponseEntity<>(userService.getAllUsers(), HttpStatus.OK); } - @GetMapping(path = "/{userId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(path = ResourceConstants.TINY_TASKS_V1_SECURED + "users/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity getUser(@PathVariable String userId) { return new ResponseEntity<>(userService.getUser(userId), HttpStatus.OK); } - @PostMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(path = ResourceConstants.TINY_TASKS_V1_UNSECURED + "users", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity createUser(@RequestBody @Valid UserRequest userRequest) { return new ResponseEntity<>(userService.createUser(userRequest), HttpStatus.CREATED); } - @PutMapping(path = "/{userId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(path = ResourceConstants.TINY_TASKS_V1_SECURED + "users/{userId}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity updateUser(@PathVariable String userId, @RequestBody @Valid UserRequest userRequest) { return new ResponseEntity<>(userService.updateUser(userRequest, userId), HttpStatus.OK); } @ResponseStatus(HttpStatus.OK) - @DeleteMapping(path = "/{userId}") + @RequestMapping(path = ResourceConstants.TINY_TASKS_V1_SECURED + "users/{userId}", method = RequestMethod.DELETE) public void deleteUser(@PathVariable String userId) { userService.deleteUser(userId); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1ba1f1d9..e3a45c19 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -31,7 +31,14 @@ spring: starttls: enable: true require: true + security: + oauth2: + resource: + filter-order: 3 + jackson: + serialization: + fail-on-empty-beans: false logging: level: com.coyoapp.tinytask: DEBUG