Thursday, 20 September, 2018 UTC


Summary

As you might have noticed in my previous blog posts, I am a big fan of Spring + Java and Spring + Kotlin. Consequently, whenever I need to implement an OAuth 2.0 authentication, spring-security-oauth2 lib is a natural choice.
However, there are next to nothing articles out there showing how to connect spring-security-oauth2 with different data sources other than inMemory and JDBC. As we have to configure a lot of stuff, I will divide this tutorial into 3 parts: How to authenticate a user, how to configure a token store and how to configure dynamic clients. So, let’s get started!
First, I am assuming that you are using one of the latest versions of spring-security-oauth2:
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>
</dependency>
Second, I am using Couchbase with Spring Data. If you are using any other data source, you can still reuse a lot of code from this blog series.
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-couchbase</artifactId>
    <version>3.0.5.RELEASE</version>
</dependency>
Additionally, I have added Lombok as a dependency to reduce Java’s boilerplate:
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
Let’s configure our Resource Server, according to spring-security-oauth2 docs: “A Resource Server (can be the same as the Authorization Server or a separate application) serves resources that are protected by the OAuth2 token. Spring OAuth provides a Spring Security authentication filter that implements this protection. You can switch it on with @EnableResourceServer on an @Configuration class, and configure it (as necessary) using a ResourceServerConfigurer”
@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);
    }
}
 
Now, let’s implement an interface called UserDetailsService. It is the interface responsible to be the bridge between your data source and Spring Security:
import com.bc.quicktask.standalone.model.CustomUserDetail;
import com.bc.quicktask.standalone.model.SecurityGroup;
import com.bc.quicktask.standalone.model.User;
import com.bc.quicktask.standalone.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.List;
import java.util.stream.Collectors;

@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository  userRepository;
    @Autowired
    private SecurityGroupService securityGroupService;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        List<User> users = userRepository.findByUsername(name);
        if(users.isEmpty()) {
            throw new UsernameNotFoundException("Could not find the user "+name);
        }

        User user = users.get(0);
        List<SecurityGroup> securityGroups = securityGroupService.listUserGroups(user.getCompanyId(), user.getId());

        return new CustomUserDetail(user, securityGroups.stream()
                .map(e->e.getId())
                .collect(Collectors.toList()) );
    }
}
In the code above, we are returning a class of the type UserDetails, which is also from Spring. Here is its implementation:
Data
public class CustomUserDetail implements UserDetails {

    private User user;
    private List<String> groups;

    public CustomUserDetail(User user, List<String> groups) {
        this.user = user;
        this.groups = groups;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return 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 user.getIsEnabled();
    }

}
I could have just made the User class implement the UserDetails directly. However, as my use case also requires the list of groups in which the user is in, I have added the implementation above.
Here are how the User, SecurityGroup and their respective repositories look like:
@Data
public class User extends BasicEntity implements Serializable {

    @Id
    @NotNull
    private String id;
    @Field
    @NotNull
    private String username;
    @Field
    @NotNull
    private String companyId;

    @Field
    @NotNull
    private String password;
    @NotNull
    private Boolean isEnabled;
    @Field
    private Boolean isVisible;
}
@N1qlPrimaryIndexed
@ViewIndexed(designDoc = "user")
public interface UserRepository extends CouchbasePagingAndSortingRepository<User, String> {

    List<User> findByUsername(String username);

}
@Document
@Data
@NoArgsConstructor
@Builder
public class SecurityGroup extends BasicEntity implements Serializable {

    @Id
    private String id;
    @NotNull
    @Field
    private String name;
    @Field
    private String description;
    @NotNull
    @Field
    private String companyId;
    @Field
    private List<String> users = new ArrayList<>();
    @Field
    private boolean removed = false;
}
@N1qlPrimaryIndexed
@ViewIndexed(designDoc = "securityGroup")
public interface SecurityGroupRepository extends
        CouchbasePagingAndSortingRepository<SecurityGroup, String> {


    @Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $1 and removed = false " +
            " AND ARRAY_CONTAINS(users, $2) ")
    List<SecurityGroup> listUserGroups(String companyId, String userId);
}
The BasicEntity class is also a small hack to better work with Spring Data and Couchbase:
public class BasicEntity {

    @Getter(PROTECTED)
    @Setter(PROTECTED)
    @Ignore
    protected String _class;
}
 
Finally, here is the implementation of our SecurityConfig class:
@Configuration
@EnableWebMvc
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService; 


    @Bean 
    @Override 
    public AuthenticationManager authenticationManagerBean() throws Exception { 
        return super.authenticationManagerBean(); 
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService)
                .passwordEncoder(encoder());
    }

    @Override
    public void configure( WebSecurity web ) throws Exception {
        web.ignoring().antMatchers( HttpMethod.OPTIONS, "/**" );
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/token").permitAll()
                .antMatchers("/api-docs/**").permitAll()
                .anyRequest().authenticated()
                .and().anonymous().disable();
    }

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Bean
    public PasswordEncoder encoder(){
        return  NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0);
        return bean;
    }
}
 
We can’t inject directly the AuthenticationManager bean anymore in Spring-Boot 2.0, but it still is required by Spring Security. Therefore, we need to implement a small hack in order to gain access to this object:
@Bean 
@Override 
public AuthenticationManager authenticationManagerBean() throws Exception { 
    return super.authenticationManagerBean(); 
}
Let’s break this class into small pieces to understand what is going on:
@Bean
public PasswordEncoder encoder(){
    return  NoOpPasswordEncoder.getInstance();
}
My user’s password is in plain text, so I just return a new instance of NoOpPasswordEncoder. A common standard is to return an instance of the BCryptPasswordEncoder class.
@Bean
public TokenStore tokenStore() {
    return new InMemoryTokenStore();
}
For now, we are going to use an in-memory token store, we will see in the part 2 how to also use Couchbase as a token store.
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(customUserDetailsService)
            .passwordEncoder(encoder());
}
Here is where the magic happens, as we are telling Spring to use our CustomUserDetailsService to search for users. This block of code is the core part of what we have done so far.
@Bean
public FilterRegistrationBean corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    source.registerCorsConfiguration("/**", config);
    FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
    bean.setOrder(0);
    return bean;
}
This block will allow us to make requests using CORS (Cross-Origin Resource Sharing)
@Override
public void configure( WebSecurity web ) throws Exception {
    web.ignoring().antMatchers( HttpMethod.OPTIONS, "/**" );
}
And finally, if you need to call your API via JQuery, you also need to add the code above. Otherwise, you will get a “Response for preflight does not have HTTP ok status.” Error.
There is just one thing left now, we need to add an Authorization Server:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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 AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    static final String CLIENT_ID = "android-client";
    static final String CLIENT_SECRET = "android-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 REFRESH_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(CLIENT_ID)
                .secret(CLIENT_SECRET)
                .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT )
                .scopes(SCOPE_READ, SCOPE_WRITE, TRUST)
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS).
                refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager);
    }


   @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
}
Well done!, now you can  start your app and call it via Postman or Jquery:
  var data = {
    "grant_type": "password",
    "username": "myuser",
    "password":"mypassword",
    "client_id":"android-client",
    "client_secret":"android-secret"
  }  

  $.ajax({
       'url': "http://localhost:8080/oauth/token",
       'type': 'POST',
       "crossDomain": true,
       "headers": { 'Authorization': 'Basic YW5kcm9pZC1jbGllbnQ6YW5kcm9pZC1zZWNyZXQ=', //android-client:android-secret in Base64
       'Content-Type':'application/x-www-form-urlencoded'},
       "data":data,
       'success': function (result) {
           console.log( "My Access token = "+ result.access_token);
           console.log( "My refresh token = "+ result.refresh_token);
           console.log("expires in = "+result.expires_in)
           succesCallback()
       },
       'error': function (XMLHttpRequest, textStatus, errorThrown) {
         errorCallback(XMLHttpRequest, textStatus, errorThrown)
       }
   });
 

Boosting performance

If you are using Couchbase, I suggest you use the username as the key of your document. It will allow you to use the Key-Value Store instead of executing N1QL queries, which will increase significantly the performance of your login.
 
If you have any questions, just tweet me at @deniswsrosa
The post How to configure an OAuth2 Authentication with Spring-Security-Oauth2 using different data sources | OAuth Part 1 appeared first on The Couchbase Blog.