I'm new to java spring and I'm trying to practise my skills through developing my own app. And now I'm trying to create authorization using jwt tokens. I wrote the loginc of it and got struggled with the small problem. I don't know why, but my principal user that I'm trying to authenticate is always null.
So here's the logic:
- First of all, here's all entities I created:
UserEntity
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String email;
private String login;
private String password;
private String name;
private String surname;
@ManyToOne
@JoinColumn(name="schools_id")
private SchoolEntity school;
@ManyToOne
@JoinColumn(name="classes_id")
private ClassEntity userClass;
@ManyToMany(fetch = FetchType.EAGER)
private List<RoleEntity> roles = new ArrayList<>();
public UserEntity() {
}
public List<RoleEntity> getRoles() {
return roles;
}
public void setRoles(List<RoleEntity> roles) {
this.roles = roles;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public long getId() {
return id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public SchoolEntity getSchool() {
return school;
}
public void setSchool(SchoolEntity school) {
this.school = school;
}
public ClassEntity getUserClass() {
return userClass;
}
public void setUserClass(ClassEntity userClass) {
this.userClass = userClass;
}
}
RoleEntity
@Entity
@Table(name = "roles")
public class RoleEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
public RoleEntity() {
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SchoolEntity
@Entity
@Table(name = "schools")
public class SchoolEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
@OneToMany(mappedBy = "school")
private List<UserEntity> user;
public SchoolEntity() {
}
public long getId() {
return id;
}
public List<UserEntity> getUser() {
return user;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
ClassEntity
@Entity
@Table(name = "classes")
public class ClassEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private int number;
private String letter;
@OneToMany(mappedBy = "userClass")
private List<UserEntity> students;
public ClassEntity() {
}
public long getId() {
return id;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getLetter() {
return letter;
}
public void setLetter(String letter) {
this.letter = letter;
}
public List<UserEntity> getStudents() {
return students;
}
public void setStudents(List<UserEntity> students) {
this.students = students;
}
}
- Then I added implementation of
UserDetailsService
to myUserService
class, where I overridedloadUserByUsername
, which finds user by login field:
Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository userRepo;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepo.findByLogin(username);
if(user == null) {
throw new UsernameNotFoundException("user with such username doesn't exists");
}
ArrayList<SimpleGrantedAuthority> userRoles = new ArrayList<>();
for(var role: user.getRoles()) {
userRoles.add(new SimpleGrantedAuthority(role.getName()));
}
User u = new User(user.getLogin(), user.getPassword(), userRoles);
return u;
}
And how it shown on the screenshot, the UserDetails
object was created:
- After this, I created the Authentication filter to make jwt tokens and authenticate users:
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(usernamePasswordAuthenticationToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authentication) throws IOException, ServletException {
UserDetails userPrincipal = (User) request.getUserPrincipal();
Algorithm algorithm = JWTConfig.getAlgorithm();
String accessToken = JWT.create()
.withSubject(userPrincipal.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() 30 * 1000 * 60))
.withClaim("roles", userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(userPrincipal.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() 60 * 1000 * 60))
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
- I also created the Authorization filter. Here it is:
public class AuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if(header != null && header.startsWith("Bearer ")) {
try {
String token = header.substring("Bearer ".length());
JWTVerifier jwtVerifier = JWT.require(JWTConfig.getAlgorithm()).build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
String login = decodedJWT.getSubject();
List<SimpleGrantedAuthority> roles = new ArrayList<>();
for (var role : decodedJWT.getClaim("roles").asArray(String.class)) {
roles.add(new SimpleGrantedAuthority(role));
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(login, null, roles);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
} catch (Exception e) {
response.setHeader("error", e.getMessage());
response.sendError(403);
}
}
else {
filterChain.doFilter(request, response);
}
}
}
- After all of it, I set up the
SecurityConfig
:
@Configuration @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthenticationFilter customAuthenticationFilter = new AuthenticationFilter(authenticationManagerBean());
customAuthenticationFilter.setFilterProcessesUrl("/api/login");
http.csrf().disable();
http.authorizeRequests().antMatchers("/api/login/**").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/users/").permitAll();
http.addFilter(customAuthenticationFilter);
http.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
But when I start the programm, it tells me that the user principal I get in AuthenticationFilter
is null. This is the line where exception was thrown:
UserDetails userPrincipal = (User) request.getUserPrincipal();
So if you know, what can be the problem, please tell me. I'd really apreciate it!
CodePudding user response:
After successful authentication Spring Security sets Authentication
object to a SecurityContextHolder
, so you can get your username
from Authentication
, injected in successfulAuthentication()
method - just change your code to this:
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
Algorithm algorithm = JWTConfig.getAlgorithm();
String accessToken = JWT.create()
.withSubject(authentication.getName()) // <- getting name here
.withExpiresAt(new Date(System.currentTimeMillis() 30 * 1000 * 60))
.withClaim("roles", authentication.getAuthorities().stream() // <- getting authorities here
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(authentication.getName()) // <- and name again here
.withExpiresAt(new Date(System.currentTimeMillis() 60 * 1000 * 60))
.sign(algorithm);
// other code
}
The reason you get null
there is that Spring only sets Principal
object to a HttpServletRequest
in SecurityContextHolderAwareRequestFilter
using SecurityContextHolderAwareRequestWrapper
, and that filter is invoked after UsernamePasswordAuthenticationFilter
.
So in your custom UsernamePasswordAuthenticationFilter
implementation Spring has not set Principal
object to the HttpServletRequest
yet.
Spring Security filter chain order can be found here in the official reference documentation.