Reputation: 239
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
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