0

I am making a RESTFul API (not web-app) and adding Spring Security but unable to do it successfully.

After going through a lot of articles and posts here on stackoverflow, I am finally posting my question. Kindly go through it and let me know what I am missing or configuring wrongly?

Base Entity

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
abstract class BaseEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "ID", nullable = false, updatable = false)
    private Long ID;

    @CreatedBy
    @Column(name = "CreatedBy", nullable = false, updatable = false)
    private String createdBy;

    @CreatedDate
    @Column(name = "CreatedDate", nullable = false, updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedBy
    @Column(name = "ModifiedBy")
    private String modifiedBy;

    @LastModifiedDate
    @Column(name = "ModifiedDate")
    private LocalDateTime modifiedDate;

    ...getters setters
}

Role Entity

@Entity
@Table(name = "ROLE")
public class Role extends BaseEntity {

    @Column(name = "Name")
    private String name;

    ...getters setters
}

User Entity

@Entity
@Table(name = "USER")
public class User extends BaseEntity {

    @Column(name = "EmiratesID", unique = true, nullable = false, updatable = false)
    private String emiratesID;

    @Column(name = "FirstName")
    private String firstName;

    @Column(name = "LastName")
    private String lastName;

    @Column(name = "StaffID", unique = true, nullable = false, updatable = false)
    private String staffID;

    @Column(name = "Email", unique = true, nullable = false)
    private String email;

    @Column(name = "Password", nullable = false)
    private String password;

    @ManyToOne(optional = false, cascade = CascadeType.MERGE)
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    ...getters setters

    public UserDetails currentUserDetails() {
        return CurrentUserDetails.create(this);
    }

}

SecurtiyConfig Class

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final DataSource dataSource;
    private final UserDetailsServiceImplementation userDetailsService;

    @Autowired
    public SecurityConfig(final DataSource dataSource, final UserDetailsServiceImplementation userDetailsService) {
        this.dataSource = dataSource;
        this.userDetailsService = userDetailsService;

    }

    @Bean
    BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                .antMatchers("/console/**").permitAll()
                .antMatchers("/", "/greetUser", "/register", "/login").permitAll()
                .antMatchers("/user/**").hasAnyAuthority(ROLES.USER.getValue(), ROLES.ADMIN.getValue())
                .antMatchers("/admin/**").hasAuthority(ROLES.ADMIN.getValue()).anyRequest().authenticated();
        httpSecurity.csrf().disable();

        // required to make H2 console work with Spring Security
        httpSecurity.headers().frameOptions().disable();

    }

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

    }

    @Override
    public void configure(WebSecurity webSecurity) {

        webSecurity.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**");
    }

CurrentUserDetails

public class CurrentUserDetails implements UserDetails {

    private String ROLE_PREFIX = "ROLE_";

    private Long userID;
    private String emiratesID;
    private String firstName;
    private String lastName;
    private String staffID;
    private String email;
    private String password;
    private Role role;

    public CurrentUserDetails(Long ID, String emiratesID, String firstName,
                              String lastName, String staffID, String email,
                              String password, Role role) {

        super();
        this.userID = ID;
        this.emiratesID = emiratesID;
        this.firstName = firstName;
        this.lastName = lastName;
        this.staffID = staffID;
        this.email = email;
        this.password = password;
        this.role = role;

    }

    public Long getUserID() {
        return userID;
    }

    public String getEmiratesID() {
        return emiratesID;
    }

    public String getEmail() {
        return this.email;
    }

    public Role getRole() {
        return this.role;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthority = new ArrayList<>();

        grantedAuthority.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));

        return grantedAuthority;
    }

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

    @Override
    public String getUsername() {
        return this.email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    /**
     * Helper method to add all details of Current User into Security User Object
     * @param user User
     * @return UserDetails
     */
    public static UserDetails create(User user) {
        return new CurrentUserDetails(user.getID(), user.getEmiratesID(),
                                      user.getFirstName(), user.getLastName(),
                                      user.getStaffID(), user.getEmail(),
                                      user.getPassword(), user.getRole());
    }

}

UserDetailsService

@Component/@Service
public class UserDetailsServiceImplementation implements UserDetailsService {

    private static final Logger userDetailsServiceImplementationLogger = LogManager.getLogger(UserDetailsServiceImplementation.class);
    private final UserRepository userRepository;

    @Autowired
    public UserDetailsServiceImplementation(final UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            userDetailsServiceImplementationLogger.error("UserDetailsServiceImplementation.loadUserByUsername() :: FAILED");

            throw new UsernameNotFoundException("UserName is not passed");
        }

        User userFound = userRepository.findByEmail(username);

        if (userFound == null) {
            userDetailsServiceImplementationLogger.error("No user found with given username = {}", username);

            throw new UsernameNotFoundException("No user found with given username");
        }

        return userFound.currentUserDetails();
    }

}

UserController Class

@RestController
@RequestMapping(value = "/user")
public class UserController {

    private static Logger userControllerLogger = LogManager.getLogger(UserController.class);

    @Autowired
    private PropertiesConfig propertiesConfig;

    @Autowired
    private UserManager userManager;

    @RequestMapping(value = "/listAll", method = RequestMethod.GET)
    public ResponseEntity<Map<String, Object>> getUsersList() {
        userControllerLogger.info("UserController.getUsersList()[/listAll] :: method call ---- STARTS");

        LinkedHashMap<String, Object> result = userManager.findAllUsers();

        userControllerLogger.info("UserController.getUsersList()[/listAll] :: method call ---- ENDS");

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

}

AdminContrller Class

@RestController
@RequestMapping(value = "/admin")
public class AdminController {

    private static final Logger adminControllerLogger = LogManager.getLogger(AdminController.class);

    private final PropertiesConfig propertiesConfig;
    private final UserManager userManager;

    @Autowired
    public AdminController(final PropertiesConfig propertiesConfig, final UserManager userManager) {
        this.propertiesConfig = propertiesConfig;
        this.userManager = userManager;

    }

    @RequestMapping(value = "/home", method = {RequestMethod.GET})
    public ResponseEntity<String> adminPortal(@RequestBody String adminName) {
        adminControllerLogger.info("AdminController.adminPortal()[/home] :: method call ---- STARTS");

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        UserDTO adminUser = userManager.findUserByEmail(auth.getName());

        if (adminUser == null) {
            throw new UsernameNotFoundException(propertiesConfig.getProperty(ApplicationProperties.Messages.NO_USER_FOUND.getValue()));
        }

        adminControllerLogger.info("AdminController.adminPortal()[/home] :: method call ---- ENDS");

        return new ResponseEntity<>(ApplicationConstants.GeneralConstants.WELCOME.getValue() + adminUser.getStaffID(), HttpStatus.OK);

    }

}

data.sql

Tried with both values ROLE_USER/ADMIN and USER/ADMIN

INSERT INTO ROLE(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, Name) VALUES (-100, 'Muhammad Faisal Hyder', now(), '', null, 'ROLE_ADMIN'/'ADMIN')
INSERT INTO ROLE(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, Name) VALUES (-101, 'Muhammad Faisal Hyder', now(), '', null, 'ROLE_USER'/'USER')

INSERT INTO USER(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, EmiratesID, FirstName, LastName, Email, StaffID, Password, ROLE_ID) VALUES (-1, 'Muhammad Faisal Hyder', now(), '', null, 'ABCDEF12345', 'Muhammad Faisal', 'Hyder', 'faisal.hyder@gmail.com', 'S776781', '$2a$10$qr.SAgYewyCOh6gFGutaWOQcCYMFqSSpbVZo.oqsc428xpwoliu7C', -100)
INSERT INTO USER(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, EmiratesID, FirstName, LastName, Email, StaffID, Password, ROLE_ID) VALUES (-2, 'Muhammad Faisal Hyder', now(), '', null, 'BCDEFG12345', 'John', 'Smith', 'John.Smith@gmail.com', 'S776741', '$2a$10$j9IjidIgwDfNGjNi8UhxAeLuoO8qgr/UH9W9.LmWJd/ohynhI7UJO', -101)

I have attached all possible classes I think which are necessary. Kindly let me know what can be the issue.

Articles I went through; SO-1, SO-2, SO-3, SO-4, Article-1, Article-2

POST Man

Resolved

@dur thanks to you for pointing it out and others as well for their helpful insights.

1- Use ROLE_ in db entries.
2- Once prefix is added in db then no need to explicitly add this in
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){...}
3- .and().httpBasic(); was missing from SpringSecurity configuration.
4- This is very detailed, might be helpful to others as well.
mfaisalhyder
  • 2,250
  • 3
  • 28
  • 38
  • Can you try changing line `.antMatchers("/admin/**").hasAuthority(ROLES.ADMIN.getValue()).anyRequest().authenticated();` to `.antMatchers("/admin/**").hasAuthority("ADMIN").anyRequest().authenticated();` ? – Avijit Barua May 06 '19 at 11:00
  • @AvijitBarua buddy, already tried it. Also added both ADMIN. ROLE_ADMIN in grantedAuthorities while overriding it. Tried both way in DB ADMIN, and ROLE_ADMIN. Nothing works. I am thinking in security description I need to tell security which field name to expect for username. BTW, when we use hasAuthority instead of hasRole, Spring adds ROLE_ prefix itself. – mfaisalhyder May 06 '19 at 17:52
  • While overriding userName in UserDetails I am returning email, so username should be expected as email... – mfaisalhyder May 07 '19 at 04:51
  • 1
    @dur 1st- I tried with both ADMIN/ROLE_ADMIN in sql. 2nd- I used both ROLE_ and without in GrantedAuthority. 3rd- I attached picture of req/res based on the created user, not the one already delcared in SQL. 4th- Issue might be I was not declaring httpBasic(). I solved it now. 5th- hasAuthority needs ROLE_ADMIN/USER because spring automatically adds it in GrantedAuthority. – mfaisalhyder May 07 '19 at 07:08

2 Answers2

0

The problem I'm seeing is that you're granting access for authority ADMIN but you're not adding this authority to the CurrentUserDetails, you're just adding their role. You should add the authority as well, i.e.

@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthority = new ArrayList<>();

        grantedAuthority.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));

        // add authority in addition to role (no role prefix)
        grantedAuthority.add(new SimpleGrantedAuthority(role.getName()));

        return grantedAuthority;
    }
msp
  • 3,272
  • 7
  • 37
  • 49
  • Thanks for answering @msparer, I also searched that and found when we use .hasAuthority we don't need to check for Prefix it is automatically added. so for granting authority I explicitly added it as I am overriding it. – mfaisalhyder May 06 '19 at 09:12
  • Still the same, 403. I am using H2, I already added few statements in data.sql. Entries are perfectly fine, even user is registered properly. but using that user/admin for querying respective resource, it screams 403 :/ One potential issue I think is that I am not telling SecurityConfig which field to expect to be passed and used a "username" but if i do that then it will be form based login, and when I do that It gives GET method not supported... – mfaisalhyder May 06 '19 at 09:24
  • While overriding userName in UserDetails I am returning email, so username should be expected as email... – mfaisalhyder May 07 '19 at 04:51
0

As @dur pointed out in comments, I am adding answer to my question.

1- Use ROLE_ in db entries.
2- Once prefix is added in db then no need to explicitly add this in
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities(){...}
3- .and().httpBasic(); was missing from SpringSecurity configuration.

Since this post is very detailed, might be helpful to others as well. For corrected answer kindly refer to my git repo

mfaisalhyder
  • 2,250
  • 3
  • 28
  • 38