ads
ads

Reputation: 1723

How to implement specific authorization by user level in spring boot?

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

Answers (1)

Garry Taylor
Garry Taylor

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

Related Questions