ServletException
ServletException

Reputation: 239

Spring Boot JWT Roles and getting 401 Unauthorized

I have a spring boot project with a /api/users/register and /api/users/login endpoint, the register endpoint works fine and allows me to POST to create a new user (With validation etc) but the login endpoint gives me a 401 (Unauthorized) response when I try to login with correct details.

Also, I am trying to add new roles to users as they are created but since I'm using JWT, I am unsure how to go about doing this, every tutorial online has a different implementation and I'm struggling to understand this too.

If someone can provide me on some steps to do this I'd appreciate it. I know you need to add the roles as claims when generating the token itself, but where do I need to implement the actual logic of assigning a new role upon creation of an account.

User.java

@Entity
@Table(name="user")
public class User{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    @NotBlank(message = "Username is required")
    private String username;

    @NotBlank(message = "Password is required")
    private String password;
    @Transient
    private String confirmPassword;

    private boolean enabled;

    @JsonFormat(pattern = "yyyy-mm-dd")
    private Date createdAt;

    // I want to load all the roles of users + the user itself once requested for
    @ManyToMany(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name="user_roles",
        joinColumns = {
            @JoinColumn(name="user_id")
        },
        inverseJoinColumns = {
            @JoinColumn(name="role_id")
    })
    private Set<Role> roles = new HashSet<>();


    // no-arg and all arg constructor
    // getters & setters




Role.java


@Entity
@Table(name="role")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String description;
    
   
    // constructors
    // getters & setters


** I do have repository/dao classes for both entities with a method to find user/role by name**


UserService.java


@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private RoleService roleService;


    public User saveUser(User user) {

        try {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            user.setUsername(user.getUsername());
            user.setConfirmPassword("");
            user.setEnabled(true);
            
            return userRepository.save(user);
        } catch(Exception e) {
            throw new UsernameAlreadyExistsException("User with username " + user.getUsername() + " already exists!");
        }
    }

}

RoleService.java


@Service
public class RoleService {

    @Autowired
    private RoleRepository roleRepository;

    public Role findByRoleName(String roleName) {
        Role theRole = roleRepository.findByName(roleName);
        return theRole;
    }
}

UserDetailsServiceImpl.java


@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if(user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        return (UserDetails) user;
    }
}

These are my JWT classes

**JwtAuthenticationFilter.java


public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

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

        try {
            String jwt = getJWTFromRequest(httpServletRequest);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {

                String username = tokenProvider.getUsernameFromJwt(jwt);
                User userDetails = (User) userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, Collections.emptyList());

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }

    private String getJWTFromRequest(HttpServletRequest request) {
        // Header Authorization: Bearer token
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}

JwtTokenProvider.java


@Component
public class JwtTokenProvider {
    // Generate the token
    public String generateToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        Date now = new Date(System.currentTimeMillis());

        Date expiryDate = new Date(now.getTime() + 300_000);

        String userId = Long.toString(user.getId());

        // this is what holds the token
        // add roles in claims
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", (Long.toString(user.getId())));
        claims.put("username", user.getUsername());
        

        return Jwts.builder().setSubject(userId).setClaims(claims).setIssuedAt(now).setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, "SECRETSECRETSECRET").compact();
    }

    // Validate the token
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey("SECRETSECRETSECRET").parseClaimsJws(token);
            return true;
        } catch (SignatureException ex) {
            System.out.println("Invalid JWT Signature");
        } catch (MalformedJwtException ex) {
            System.out.println("Invalid JWT Token");
        } catch (ExpiredJwtException ex) {
            System.out.println("Expired JWT Token");
        } catch (UnsupportedJwtException ex) {
            System.out.println("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            System.out.println("JWT claims string is empty");
        }
        return false;
    }

    public String getUsernameFromJwt(String token) {
        Claims claims = Jwts.parser().setSigningKey("SECRETSECRETSECRET").parseClaimsJws(token).getBody();

        return claims.getSubject();
    }
}


JwtAuthenticationEntryPoint.java


@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        InvalidLoginResponse loginResponse = new InvalidLoginResponse();

        
        // InvalidLoginResponse is a class with username and password fields and a no-arg 
        constructor initialiting them like this

        //  this.username = "Invalid Username";
        //  this.password = "Invalid Password";

        String jsonLoginResponse = new Gson().toJson(loginResponse);

        httpServletResponse.setContentType("application/json");
        httpServletResponse.setStatus(401);
        httpServletResponse.getWriter().print("ERROR FROM JwtAuthenticationEntryPoint: " + jsonLoginResponse);
    }
}


SecurityConfig.java


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

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

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

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


    // Here i am permitting access to all URLs but getting 401 when posting to /login
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                .anyRequest().permitAll();

        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

}

UserController.java


@RestController
@RequestMapping("/api/users")
@CrossOrigin
public class UserController {

    private UserService userService;
    private UserDetailsServiceImpl userDetailsService;
    private UserValidator userValidator;
    private ErrorValidationService errorValidationService;
    private JwtTokenProvider tokenProvider;
    private AuthenticationManager authenticationManager;

    @Autowired
    public UserController(UserService userService, UserDetailsServiceImpl userDetailsService, UserValidator userValidator, ErrorValidationService errorValidationService, JwtTokenProvider tokenProvider, AuthenticationManager authenticationManager) {
        this.userService = userService;
        this.userDetailsService = userDetailsService;
        this.userValidator = userValidator;
        this.errorValidationService = errorValidationService;
        this.tokenProvider = tokenProvider;
        this.authenticationManager = authenticationManager;
    }

    // I want to allow role based access to these URLs
    @GetMapping("/all")
    public String welcomeAll() {
        return "Anyone can view this!";
    }

    @GetMapping("/admin")
    public String adminPing(){
        return "Only Admins Can view This";
    }

    @GetMapping("/user")
    public String userPing(){
        return "Any User Can view This";
    }


    @PostMapping("/register")
    public ResponseEntity<?> register(@Valid @RequestBody User user, BindingResult result) {

        userValidator.validate(user, result);
        ResponseEntity<?> errorMap = errorValidationService.validationService(result);
        if(errorMap != null) return errorMap;

        User newUser = userService.saveUser(user);

        return new ResponseEntity<User>(newUser, HttpStatus.CREATED);
    }



    @PostMapping("/login")
    public ResponseEntity<?> generateToken(@Valid @RequestBody LoginRequest loginRequest, BindingResult result) throws Exception {

        System.out.println("Entering /login");
        ResponseEntity<?> errorMap = errorValidationService.validationService(result);
        if(errorMap != null) return errorMap;

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = "Bearer " + tokenProvider.generateToken(authentication);

        return ResponseEntity.ok(new JwtLoginSuccessResponse(true, jwt));

    }
}

When trying to register in postman /api/users/register


When trying to login in postman /api/users/register

Upvotes: 0

Views: 2833

Answers (1)

I AM GROOT
I AM GROOT

Reputation: 291

As you added OncePerRequestFilter for all the incoming request. So when you call /api/users/login it will check for JWT token and unable to find it in header so it is throwing 401 (Unauthorized). To exclude /api/users/login end point from OncePerRequestFilter you need to override shouldNotFilter(HttpServletRequest request)

public class LoginFilter extends OncePerRequestFilter {

        private List<String> excludeUrlPatterns = new ArrayList<String>();

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

       @Override
       protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException 
       {
           // add excludeUrlPatterns on which one to exclude here
       }

    }

For more info visit :OncePerRequestFilter

Upvotes: 2

Related Questions