Reputation: 1723
I have created a web service using Spring Boot 1.2.1.RELEASE. This has POST ang GET methods which I defined in a controller. I have also configured authentication using my own user credentials from the application database and have implemented OAuth as well.
Here's my Application.class which has all the necessary configuration.
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(Application.class);
}
@Service
protected static class ApplicationUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username);
if (user == null) {
return null;
}
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
String password = user.getPassword();
return new org.springframework.security.core.userdetails.User(username, password, auth);
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ApplicationUserDetailsService applicationUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(applicationUserDetailsService);
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
@Configuration
@EnableResourceServer
protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/user/**").access("#oauth2.hasScope('read')")
.antMatchers("/car/**").access("#oauth2.hasScope('read')")
.antMatchers("/transaction/**").access("#oauth2.hasScope('read')");
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfig extends
AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("my-trusted-client")
.authorizedGrantTypes("authorization_code", "password",
"refresh_token")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(60);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
security.allowFormAuthenticationForClients();
}
}
}
Here's my pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo.web.restservice</groupId>
<artifactId>restservice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>restservice</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>com.demo.web.restservice.Application</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <scope>runtime</scope> -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- Added for security purposes only -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.6.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>jackson-mapper-asl</artifactId>
<groupId>org.codehaus.jackson</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
And here's one of my controller.
@RestController
@RequestMapping("/transaction")
public class TransactionController {
@Autowired
private TransactionHeaderRepository transactionHeaderRepository;
@RequestMapping("/header/{id}")
public ResponseEntity<TransactionHeader> findHeaderById(@PathVariable UUID id) {
return new ResponseEntity<>(this.transactionHeaderRepository.findById(id), HttpStatus.OK);
}
@RequestMapping(value = "/header/create", method = RequestMethod.POST)
public ResponseEntity<TransactionHeader> createHeader(@RequestBody TransactionHeader transactionHeader) {
this.transactionHeaderRepository.save(transactionHeader);
this.transactionLineRepository.save(transactionHeader.getTransactionLines());
return new ResponseEntity<>(HttpStatus.OK);
}
@RequestMapping(value = "/header/update", method = RequestMethod.POST)
public ResponseEntity<TransactionHeader> updateHeader(@RequestBody TransactionHeader transactionHeader) {
return new ResponseEntity<TransactionHeader>(this.transactionHeaderRepository.save(transactionHeader), HttpStatus.OK);
}
@RequestMapping(value = "/header/delete", method = RequestMethod.POST)
public ResponseEntity<TransactionHeader> deleteHeader(@RequestBody TransactionHeader transactionHeader) {
this.transactionHeaderRepository.delete(transactionHeader);
return new ResponseEntity<TransactionHeader>(HttpStatus.OK);
}
}
Everything works fine except for the part which I want to restrict users and allow them to update only their own data.
How should I go about doing this? Should this be done in the controller? Or does spring provided some sort of built-in functionality to handle this?
Appreciate your feedback.
Upvotes: 1
Views: 958
Reputation: 1115
To do this you can create your own custom annotation (@Useridentifier) and use this to map to UserDetails.UserId (your users PK). Place this on your DTO that contains the user PK.
Next, you can create another annotation (@Useridentifiercheck) that you place on your endpoints (Like requestMapping). If the value does not match then throw an exception and return 401 or replace the userId with that from OAuth attributes (if you store the userId in the OAuth data).
NOTE: All this can also be done with a Service and some reflection but I like annotations :)
Where this fails If you have a rest point that updates FK tables with no UserId then this idea falls over.
Example
table - User { long userId; nvarchar name; long address_fk; }
table - Address { long addressId; nvarchar postcode; }
Here we can update the address, but without some join to give us the userId, we can't know if we are "authorised" to edit the address.
Server Side Issues need to be careful to remove this feature for admins, otherwise, they won't be able to update other peoples data ether...
Gateway API
Another example is to create a Gateway API that sits close to your UI (For example ExpressJS servicing React and a custom API)
The UI session can be stored within Express and will filter to make sure that any requests to update automatically have the "correct" UserID.
For example: A bad guy send the following JSON
put: { userId: 69, password: powned, firstname: Paul }
The Express API takes the DTO and strips non-authorised inputs (userId, password). Note, in this case, the request will be then delivered to the API replacing the "passed in" userId with the correct session value.
The core API will sit behind the VPN firewall and restricted using the Customer OAuth Token.
Upvotes: 1