What's a correct way to work with users using JWT authentication?

Viewed 192

I've created backend for my mobile application with REST API and JWT authentication/authorization.
Then I created android application using Retrofit.
After retrieving JWToken from /login endpoint I've created GET request on a server-side to parse username of currently logged user with token, and then I call method on client-side.

UserController.java (server-side)

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

    private UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public String getCurrentUser(@AuthenticationPrincipal Object user) {

        user = SecurityContextHolder.getContext().getAuthentication()
                .getPrincipal();

        return user.toString();
    }
}

But I'm not sure if it's a right way to develop it like this.

Let's say I have two tables in my database.

  • One with login credentials that are being used in Authentication
  • Second with users personal data

enter image description here

and now I want to display First name and last name of a user.

Now, the only information after login I have is users username that he logged with and if I want to get more information I have to somehow make Queries on client side to:

  • first - get id of a user where username = username that I got from token
  • then - get object of users_data where user_id = id from the first query

and I don't think this process should be done on the client side(correct me if I'm wrong, please).

Question

So my question is what should I do fulfill this scenario where I want to get all information about user where I have only his username in client-side app. Should I make changes in my backend, or stick to making queries from mobile app?

(Server-side)

AuthenticationFilter.java

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws AuthenticationException {

        // Mapping credentials to loginviewmodel
        LoginViewModel credentials = null;
        try {
            credentials = new ObjectMapper().readValue(request.getInputStream(), LoginViewModel.class);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Creating login token
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                credentials.getUsername(),
                credentials.getPassword(),
                new ArrayList<>()
        );

        // Authenticate user
        Authentication auth = authenticationManager.authenticate(authenticationToken);
        return auth;
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authResult
    ) throws IOException, ServletException {

        // Grab current user
        UserImpl principal = (UserImpl) authResult.getPrincipal();

        // Create JWT Token
        String token = JWT.create()
                .withSubject(principal.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + JwtProperties.EXPIRATION_TIME))
                .sign(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()));

        // Add token in response(this is syntax of token)
        response.addHeader(JwtProperties.HEADER_STRING, JwtProperties.TOKEN_PREFIX + token);

    }
}

AuthorizationFilter.java

public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserRepository userRepository;

    public JwtAuthorizationFilter(
            AuthenticationManager authenticationManager,
            UserRepository userRepository
    ) {
        super(authenticationManager);
        this.userRepository = userRepository;
    }

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

        // Read authorization header with JWT Token
        String header = request.getHeader(JwtProperties.HEADER_STRING);

        if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
        }

        // Try get user data from DB to authorize
        Authentication authentication = getUsernamePasswordAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        chain.doFilter(request, response);
    }

    private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) {

        String token = request.getHeader(JwtProperties.HEADER_STRING);

        if (token != null) {

            // parse and validate token
            String username = JWT.require(Algorithm.HMAC512(JwtProperties.SECRET.getBytes()))
                    .build()
                    .verify(token.replace(JwtProperties.TOKEN_PREFIX, ""))
                    .getSubject();

            if (username != null) {

                User user = userRepository.findByUsername(username);
                UserImpl principal = new UserImpl(user);
                UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, principal.getAuthorities());
                return auth;
            }
            return null;
        }
        return null;
    }
}

UserImpl.java

public class UserImpl implements UserDetails {

    private User user;

    public UserImpl(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        List<GrantedAuthority> authorities = new ArrayList<>();

        // Get list of roles (ROLE_name)
        this.user.getRoleList().forEach( role -> {
            GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
            authorities.add(authority);
        });

        return authorities;
    }

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

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

(Client-side)

Method for parsing username from currently logged in User:

public void getCurrentUser() {

        Call<String> call = ApiClient.getUserService(getApplicationContext()).getCurrentUser();
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(Call<String> call, Response<String> response) {

                if (response.isSuccessful()) {

                    String user = response.body();
                    nameOfUserView.setText(user);
                }
            }

            @Override
            public void onFailure(Call<String> call, Throwable t) {
                nameOfUserView.setText(t.getMessage());
            }
        });
}
1 Answers

There is a loophole in your logic. Lets see

#1 - It is fine. Based on JWT Token, you are fetching username #2 - Using token in header, you are fetching other details by sending username.

In #2, what if I send username of any other user instead of logged in user. System will still respond with the details. So, any logged in user will be able to see details of any user.

To handle this, you should use some DTO class which has all required fields say UserReturnData. Structure will be

public class UserReturnData 
{
    String username;
    List<String> roles;
    Long id;
//more fields as per requirement
}

Then in your current user call, populate this data based on authorisation header. Do not send any username. Authorisation header should be sufficient to fetch user details. Sample:

public UserReturnData fetchUserDetails()
    {
        UserReturnData userReturnData = new UserReturnData();
        List<String> roles = new ArrayList<String>();

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        userReturnData.setUsername(auth.getName());
        Long id = uRepo.findId(auth.getName());
        userReturnData.setId(id);
        Collection<SimpleGrantedAuthority> authorities = (Collection<SimpleGrantedAuthority>) SecurityContextHolder
                .getContext().getAuthentication().getAuthorities();
        for (SimpleGrantedAuthority authority : authorities)
        {
            roles.add(authority.getAuthority());
        }
        userReturnData.setRoles(roles);

//Populate other required fields here. 
        return userReturnData;
    }

Whenever you need details of logged-in user. You can make call to current user API with only Authorization token and get logged in user information

Related