Skip to content

Commit

Permalink
Adding OAuth2 Security mindsmash#33
Browse files Browse the repository at this point in the history
- User can login
- User can register
- Only authorized user can access tasks and some user data
  • Loading branch information
ymolla committed Nov 8, 2019
1 parent dde7dc0 commit 490e11f
Show file tree
Hide file tree
Showing 14 changed files with 320 additions and 14 deletions.
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

}
5 changes: 3 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions src/main/java/com/coyoapp/tinytask/ResourceConstants.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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 * * *";

//used only for testing
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/";
}
63 changes: 63 additions & 0 deletions src/main/java/com/coyoapp/tinytask/auth/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> getAuthorities() {
if(null == user.getRoles()) {
return Collections.emptySet();
}
Set<SimpleGrantedAuthority> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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<UserResponse> getAllUsers() {
List<User> users = (List<User>) userRepository.findAll();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/coyoapp/tinytask/web/TaskController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

@Slf4j
@RestController
@RequestMapping(ResourceConstants.TINY_TASKS_V1 + "tasks")
@RequestMapping(ResourceConstants.TINY_TASKS_V1_SECURED + "tasks")
@RequiredArgsConstructor
public class TaskController {

Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/coyoapp/tinytask/web/TinyTaskController.java
Original file line number Diff line number Diff line change
@@ -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<List<TaskResponse>> getAllUserTasks(@PathVariable String userId) {
return new ResponseEntity<>(tinyTaskService.getAllUserTasks(userId), HttpStatus.OK);
}

@GetMapping(path = "current-user",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<User> getLoggedInUser() {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

String username = principal.getUsername();

User user = userRepository.findByUsername(username);

return new ResponseEntity<>(user, HttpStatus.OK);
}
}
Loading

0 comments on commit 490e11f

Please sign in to comment.