Hot questions for Spring Security REST API

Top 10 Java Open Source / Spring / Spring Security REST API

Question:

I want to use Spring Security for JWT authentication. But it comes with default authentication. I am trying to disable it, but the old approach of doing this - disabling it through application.properties - is deprecated in 2.0.

This is what I tried:

@Configuration
public class StackWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic().disable();
        // http.authorizeRequests().anyRequest().permitAll(); // Also doesn't work.
    }
}

How can I simply disable basic security?

UPDATE It might be nice to know that I am not using web mvc but web flux.

Screenshot:


Answer:

According to the new updates in Spring 2.0, if Spring Security is on the classpath, Spring Boot will add @EnableWebSecurity.So adding entries to the application.properties ain't gonna work (i.e it is no longer customizable that way). For more information visit the official website Security changes in Spring Boot 2.0

Albeit not sure about your requirement exactly, I could think of one workaround like the following:-

@Configuration
@EnableWebSecurity
public class SecurityConfiguration  extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests().antMatchers("/").permitAll();
    }
}

Hope this helps.

Question:

I'm working on a grails rest app. The grails version I'm using is 3.3.1. I'm using spring-security-rest for authorization. I've created the following classes using the s2-quickstart command.

  1. User
  2. Authority
  3. UserAuthority

The app runs fine but the unit tests for the User class fail with the following error in console.

java.lang.IllegalStateException: Either class [hungr.Authority] is not a domain class or GORM has not been initialized correctly or has already been shutdown. Ensure GORM is loaded and configured correctly before calling any methods on a GORM entity.
at org.grails.datastore.gorm.GormEnhancer.stateException(GormEnhancer.groovy:469)
at org.grails.datastore.gorm.GormEnhancer.findStaticApi(GormEnhancer.groovy:300)
at org.grails.datastore.gorm.GormEnhancer.findStaticApi(GormEnhancer.groovy:296)
at org.grails.datastore.gorm.GormEntity$Trait$Helper.currentGormStaticApi(GormEntity.groovy:1349)
at org.grails.datastore.gorm.GormEntity$Trait$Helper.staticMethodMissing(GormEntity.groovy:756)
at hungr.UserController.$tt__save(UserController.groovy:39)
at hungr.UserController.save_closure1(UserController.groovy)
at groovy.lang.Closure.call(Closure.java:414)
at groovy.lang.Closure.call(Closure.java:430)
at grails.gorm.transactions.GrailsTransactionTemplate$2.doInTransaction(GrailsTransactionTemplate.groovy:94)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:133)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
at org.grails.testing.runtime.support.ActionSettingMethodHandler.invoke(ActionSettingMethodHandler.groovy:28)
at hungr.UserControllerSpec.Test the save action correctly persists(UserControllerSpec.groovy:90)

I've tried referring to the answer at GORM fails to realize Domain classes from a plugin are GORM classes but nothing worked. I'm pretty new to grails hence I have no clue what may be going wrong. the classes I'm using are:

User.Groovy

@GrailsCompileStatic
@EqualsAndHashCode(includes='username')
@ToString(includes='username', includeNames=true, includePackage=false)
class User implements Serializable, UserDetails {

private static final long serialVersionUID = 1
String username
String password
boolean enabled = true
boolean accountExpired
boolean accountLocked
boolean passwordExpired
String name
String email
Integer age
Boolean isVeg
byte[] profilePicture
String profilePictureContentType
String facebookId
String facebookProfilePictureUrl
boolean  isFacebookUser
static hasMany = [notifications: Notification, posts: DiaryItem, comments: Comment]
Set<Authority> getAuthorities() {
    (UserAuthority.findAllByUser(this) as List<UserAuthority>)*.authority as Set<Authority>
}

@Override
boolean isAccountNonExpired() {
    return !accountExpired
}

@Override
boolean isAccountNonLocked() {
    return !accountLocked
}

@Override
boolean isCredentialsNonExpired() {
    return !passwordExpired
}

static constraints = {
    password nullable: false, blank: false, password: true
    username nullable: false, blank: false, unique: true
    facebookId nullable: true
    name nullable: false, blank: false, maxSize: 100
    email blank: false, email: true
    age nullable: false, min: 8
    isVeg nullable: false
    profilePicture nullable: true, maxSize: 1073741824
    profilePictureContentType nullable: true
    isFacebookUser nullable: false
    facebookProfilePictureUrl nullable: true, maxSize: 1000
}

static mapping = {
    password column: '`password`'
}
}

Authority.Groovy

@GrailsCompileStatic
@EqualsAndHashCode(includes='authority')
@ToString(includes='authority', includeNames=true, includePackage=false)
class Authority implements Serializable, GrantedAuthority {

    private static final long serialVersionUID = 1

    String authority

    static constraints = {
    authority nullable: false, blank: false, unique: true
    }

    static mapping = {
    cache true
    }
}

UserAuthority.Groovy

@GrailsCompileStatic
@ToString(cache=true, includeNames=true, includePackage=false)
class UserAuthority implements Serializable {

private static final long serialVersionUID = 1

User user
Authority authority

@Override
boolean equals(other) {
    if (other instanceof UserAuthority) {
        other.userId == user?.id && other.authorityId == authority?.id
    }
}

@Override
int hashCode() {
    int hashCode = HashCodeHelper.initHash()
    if (user) {
        hashCode = HashCodeHelper.updateHash(hashCode, user.id)
    }
    if (authority) {
        hashCode = HashCodeHelper.updateHash(hashCode, authority.id)
    }
    hashCode
}

static UserAuthority get(long userId, long authorityId) {
    criteriaFor(userId, authorityId).get()
}

static boolean exists(long userId, long authorityId) {
    criteriaFor(userId, authorityId).count()
}

private static DetachedCriteria criteriaFor(long userId, long authorityId) {
    UserAuthority.where {
        user == User.load(userId) &&
        authority == Authority.load(authorityId)
    }
}

static UserAuthority create(User user, Authority authority, boolean flush = false) {
    def instance = new UserAuthority(user: user, authority: authority)
    instance.save(flush: flush)
    instance
}

static boolean remove(User u, Authority r) {
    if (u != null && r != null) {
        UserAuthority.where { user == u && authority == r }.deleteAll()
    }
}

static int removeAll(User u) {
    u == null ? 0 : UserAuthority.where { user == u }.deleteAll() as int
}

static int removeAll(Authority r) {
    r == null ? 0 : UserAuthority.where { authority == r }.deleteAll() as int
}

static constraints = {
    user nullable: false
    authority nullable: false, validator: { Authority r, UserAuthority ur ->
        if (ur.user?.id) {
            if (UserAuthority.exists(ur.user.id, r.id)) {
                return ['userRole.exists']
            }
        }
    }
}

static mapping = {
    id composite: ['user', 'authority']
    version false
}
}

EDIT 1: The Unit Test class is:

class UserControllerSpec extends Specification implements          
ControllerUnitTest<UserController>, DomainUnitTest<User> {

def populateValidParams(params) {
    assert params != null

    // TODO: Populate valid properties like...
    //params["name"] = 'someValidName'
    params["username"]
    params["password"]
    params["name"] = "User"
    params["email"] = "user@hungr.com"
    params["age"] = 19
    params["isVeg"] = false
     //       new MockMultipartFile('profilePicture', 'myImage.jpg', imgContentType, imgContentBytes)

   // def multipartFile = new GrailsMockMultipartFile('profilePicture', 'profilePicture.jpg', 'image/jpeg', new byte[0])
    //request.addFile(multipartFile)
  //  params["profilePicture"] =// new MockMultipartFile('profilePicture', 'file.jpg', 'image/jpeg', "1234567" as byte[])
    params["profilePictureContentType"] = "image/jpeg"
    params["facebookId"] = "fb_id"
    params["facebookProfilePictureUrl"] = "http://abc.def"
    params["isFacebookUser"] = true
    //assert false, "TODO: Provide a populateValidParams() implementation for this generated test suite"
}

void "Test the index action returns the correct model"() {
    given:
    controller.userService = Mock(UserService) {
        1 * list(_) >> []
        1 * count() >> 0
    }

    when:"The index action is executed"
    controller.index()

    then:"The model is correct"
    !model.userList
    model.userCount == 0
}

void "Test the create action returns the correct model"() {
    when:"The create action is executed"
    controller.create()

    then:"The model is correctly created"
    model.user!= null
}

void "Test the save action with a null instance"() {
    when:"Save is called for a domain instance that doesn't exist"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'POST'
    request.format = 'form'
    controller.save(null)

    then:"A 404 error is returned"
    response.redirectedUrl == '/user/index'
    flash.message != null
}

void "Test the save action correctly persists"() {
    given:
    controller.userService = Mock(UserService) {
        1 * save(_ as User)
    }

    when:"The save action is executed with a valid instance"
    response.reset()
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'POST'
    request.format = 'form'
    byte[] b = new byte[1]
    b[0]= 123
    request.addFile(new MockMultipartFile('profilePicture', 'file.jpg', 'image/jpeg', b))
    populateValidParams(params)
    def user = new User(params)
    user.id = 1

    controller.save(user)

    then:"A redirect is issued to the show action"
    response.redirectedUrl == '/user/show/1'
    controller.flash.message != null
}

void "Test the save action with an invalid instance"() {
    given:
    controller.userService = Mock(UserService) {
        1 * save(_ as User) >> { User user ->
            throw new ValidationException("Invalid instance", user.errors)
        }
    }

    when:"The save action is executed with an invalid instance"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'POST'
    def user = new User()
    controller.save(user)

    then:"The create view is rendered again with the correct model"
    model.user != null
    view == 'create'
}

void "Test the show action with a null id"() {
    given:
    controller.userService = Mock(UserService) {
        1 * get(null) >> null
    }

    when:"The show action is executed with a null domain"
    controller.show(null)

    then:"A 404 error is returned"
    response.status == 404
}

void "Test the show action with a valid id"() {
    given:
    controller.userService = Mock(UserService) {
        1 * get(2) >> new User()
    }

    when:"A domain instance is passed to the show action"
    controller.show(2)

    then:"A model is populated containing the domain instance"
    model.user instanceof User
}

void "Test the edit action with a null id"() {
    given:
    controller.userService = Mock(UserService) {
        1 * get(null) >> null
    }

    when:"The show action is executed with a null domain"
    controller.edit(null)

    then:"A 404 error is returned"
    response.status == 404
}

void "Test the edit action with a valid id"() {
    given:
    controller.userService = Mock(UserService) {
        1 * get(2) >> new User()
    }

    when:"A domain instance is passed to the show action"
    controller.edit(2)

    then:"A model is populated containing the domain instance"
    model.user instanceof User
}


void "Test the update action with a null instance"() {
    when:"Save is called for a domain instance that doesn't exist"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'PUT'
    controller.update(null)

    then:"A 404 error is returned"
    response.redirectedUrl == '/user/index'
    flash.message != null
}

void "Test the update action correctly persists"() {
    given:
    controller.userService = Mock(UserService) {
        1 * save(_ as User)
    }

    when:"The save action is executed with a valid instance"
    response.reset()
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'PUT'
    request.format = 'form'
    request.addFile(new MockMultipartFile('profilePicture', 'file.jpg', 'image/jpeg', "1234567" as byte[]))
    populateValidParams(params)
    def user = new User(params)
    user.id = 1

    controller.update(user)

    then:"A redirect is issued to the show action"
    response.redirectedUrl == '/user/show/1'
    controller.flash.message != null
}

void "Test the update action with an invalid instance"() {
    given:
    controller.userService = Mock(UserService) {
        1 * save(_ as User) >> { User user ->
            throw new ValidationException("Invalid instance", user.errors)
        }
    }

    when:"The save action is executed with an invalid instance"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'PUT'
    controller.update(new User())

    then:"The edit view is rendered again with the correct model"
    model.user != null
    view == 'edit'
}

void "Test the delete action with a null instance"() {
    when:"The delete action is called for a null instance"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'DELETE'
    controller.delete(null)

    then:"A 404 is returned"
    response.redirectedUrl == '/user/index'
    flash.message != null
}

void "Test the delete action with an instance"() {
    given:
    controller.userService = Mock(UserService) {
        1 * delete(2)
    }

    when:"The domain instance is passed to the delete action"
    request.contentType = FORM_CONTENT_TYPE
    request.method = 'DELETE'
    controller.delete(2)

    then:"The user is redirected to index"
    response.redirectedUrl == '/user/index'
    flash.message != null
}
}

Answer:

So normally in a Unit test you test a single unit, User in this case. Because you want to test additional entities, you need to add them to the test. You can do this by implementing getDomainClassesToMock. Best is to use the DataTest trait instead of the DomainUnitTest in this situation (DomainUnitTest extends DataTest).

So your test should look like:

class UserControllerSpec extends Specification implements          
ControllerUnitTest<UserController>, DataTest {

    Class<?>[] getDomainClassesToMock(){
        return [User,Authority,UserAuthority] as Class[]
    }
    .... 
}

Question:

Spring security configuration class

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .cors()
            .and()
            .authorizeRequests()
            .antMatchers("/user", "/login").permitAll()
            .antMatchers("/employee", "/insurance").hasRole("User")
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic()
            .and()
            .csrf().disable();
    }

    protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }
}

UserDetailsService Implementation class

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        User user = null;
        Set<GrantedAuthority> grantedAuthorities = null;
        try
        {
            user = userService.findByUserName(userName);
            if(user == null)
                throw new UsernameNotFoundException("User " + userName  + " not available");

            grantedAuthorities = new HashSet<>();
            for(Role role: user.getRoles()) {
                grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole().toString()));
            }
        }
        catch(Exception exp) {
            exp.printStackTrace();
        }
        return new org.springframework.security.core.userdetails.User(user.getUserName(), user.getPassword(), grantedAuthorities);
    }
}

Employee Rest Controller class

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @Autowired
    private InsuranceService insuranceService;

    @PostMapping("/employee")
    public ResponseEntity<Employee> create(@RequestBody Employee employee) throws Exception {
        employee = employeeService.create(employee);
        return new ResponseEntity<Employee>(employee, HttpStatus.CREATED);
    }

    @PutMapping("/employee")
    public ResponseEntity<Employee> update(@RequestBody Employee employee) throws Exception {
        employee = employeeService.update(employee);
        return new ResponseEntity<Employee>(employee, HttpStatus.OK);
    }

    @DeleteMapping("/employee/{id}")
    public ResponseEntity<String> delete(@PathVariable("id") long id) throws Exception {
        employeeService.delete(id);
        return new ResponseEntity<String>("Employee deleted successfully", HttpStatus.OK);
    }

    @GetMapping("/employee/{id}")
    public ResponseEntity<Employee> findEmployeeDetails(@PathVariable("id") long id) throws Exception {
        Employee employee = employeeService.findById(id);
        return new ResponseEntity<Employee>(employee, HttpStatus.OK);
    }

    @GetMapping("/employee")
    public ResponseEntity<List<Employee>> findAll() throws Exception {
        List<Employee> employees = employeeService.findAll();
        return new ResponseEntity<List<Employee>>(employees, HttpStatus.OK);
    }
}

I am getting 403 forbidden error for any of the HTTP method(POST/GET/PUT) request submitted via postman to /employee URL

{
    "timestamp": "2019-09-17T05:37:35.778+0000",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/hr-core/employee"
}

I am getting this error even though I am sending correct username & password in the basic auth header(Authorization) of HTTP request in POSTMAN. This user is also having both USER and ADMIN roles to access /employee REST endpoint. I have disabled CSRF in http security.

How can I solve this error?


Answer:

Within Spring Security, there is a difference between roles and authorities. While an authority can be anything, roles are a subset of authorities that start with ROLE_.

Let's say you have the following authorities:

GrantedAuthority authority1 = new SimpleGrantedAuthority("User");
GrantedAuthority authority2 = new SimpleGrantedAuthority("ROLE_Admin");

In this case, authority1 does not contain a role, while authority2 does because it's prefixed with ROLE_.

That means, that if you use hasRole("User"), you won't have access, because it's not defined as a role. hasRole("Admin") on the other hand would work.

To solve this, you have two options:

  1. Make sure your roles are really prefixed with ROLE_. If you don't store them that way in your database, you can modify your UserDetailsServiceImpl:

    String roleName = "ROLE_" + role.getRole().toString();
    grantedAuthorities.add(new SimpleGrantedAuthority(roleName));
    
  2. Alternatively, you can use hasAuthority("User") instead:

    // ...
    .antMatchers("/employee", "/insurance").hasAuthority("User")
    // ...
    

Question:

I'm writing a filter that would intercept an Restful API call , extract a Bearer token and make a call to an Authorization Server for validation.

I couldn't find one in Spring Boot that does it out of the box, but I'm sure there is a cleaner way to do this. here is what I have (pseudo code):

public class SOOTokenValidationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    String xAuth = request.getHeader("Authorization");

    // validate the value in xAuth
    if(isValid(xAuth) == false){
        throw new SecurityException();
    }  

    // Create our Authentication and set it in Spring 
      Authentication auth = new Authentication ();
      SecurityContextHolder.getContext().setAuthentication(auth);            

    filterChain.doFilter(request, response);

}
private boolean isValid (String token){

    // make a call to SSO passing the access token and 
    // return true if validated
    return true;
}

}


Answer:

Lessons learned, Spring Security Oauth2 documentation is woefully inadequate, forget about trying to use the framework without fully combing through the source code. On the flip side the code is well written and easy to follow kudos to Dave Syer.

Here is my config:

protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();                  
    http.authorizeRequests()
        .antMatchers("/")
        .permitAll()
        .and()      
        .addFilterBefore(getOAuth2AuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
        .exceptionHandling();                        
}

Here is my getOAuth2AuthenticationProcessingFilter method:

private OAuth2AuthenticationProcessingFilter getOAuth2AuthenticationProcessingFilter() {       
    // configure token Extractor
    BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();
    // configure Auth manager
    OAuth2AuthenticationManager manager = new OAuth2AuthenticationManager();
    // configure RemoteTokenServices with your client Id and auth server endpoint
    manager.setTokenServices(remoteTokenServices);

    OAuth2AuthenticationProcessingFilter filter = new OAuth2AuthenticationProcessingFilter();
    filter.setTokenExtractor(tokenExtractor);        
    filter.setAuthenticationManager(manager);
    return filter;
}