Authenticating jwt for multiple users in spring boot
Asked Answered
N

0

0

I have the following application.

SpringMainApplication.java

@SpringBootApplication
public class SpringMainApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(SpringMainApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringMainApplication.class, args);
    }

}

@RestController
class MainController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtTokenUtil;

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Value("${token.expiration.time}")
    private int tokenExpirationTime;

    @RequestMapping(value = "/service1/access-token", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {

        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
        }
        catch (BadCredentialsException e) {
            throw new Exception("Incorrect password", e);
        }

        final UserDetails userDetails = userDetailsService
            .loadUserByUsername(authenticationRequest.getUsername());

        final String jwt = jwtTokenUtil.generateToken(userDetails);

        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.set("expiration", String.valueOf(tokenExpirationTime / 1000));

        return ResponseEntity.ok().headers(responseHeaders).body(new AuthenticationResponse(jwt));
    }

    @RequestMapping(value = "/service1/test", consumes = "application/json", method = RequestMethod.GET)
    public ResponseEntity<?> checkServerStatus() throws Exception {
        // SOME CODE
    }

    @RequestMapping(value = "/service2/access-token", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {

        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
        }
        catch (BadCredentialsException e) {
            throw new Exception("Incorrect password", e);
        }

        final UserDetails userDetails = userDetailsService
            .loadUserByUsername(authenticationRequest.getUsername());

        final String jwt = jwtTokenUtil.generateToken(userDetails);

        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.set("expiration", String.valueOf(tokenExpirationTime / 1000));

        return ResponseEntity.ok().headers(responseHeaders).body(new AuthenticationResponse(jwt));
    }

    @RequestMapping(value = "/service2/test", consumes = "application/json", method = RequestMethod.GET)
    public ResponseEntity<?> checkServerStatus() throws Exception {
        // SOME CODE
    }

@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService myUserDetailsService;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

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

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
            .authorizeRequests().antMatchers("/**/service1/access-token").permitAll().
            anyRequest().authenticated().and().
            exceptionHandling().and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

MyUserDetailsService.java

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Value("${username1}")
    private String username;

    @Value("${password1}")
    private String password;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if (username != null && username.equals(username1)) {
            return new User(username, password1, new ArrayList<>());
        } else {
            throw new UsernameNotFoundException("Username not found: " + username);
        }
    }

}

JwtRequestFilter.java

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

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

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        StringBuilder sb = new StringBuilder();

        try {
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                jwt = authorizationHeader.substring(7);
                username = jwtUtil.extractUsername(jwt);
            }
        } catch (UnsupportedJwtException e) {
            sb = buildResponseBody(request, e.getMessage());
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write(sb.toString());
            return;
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtUtil.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                    .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

JwtUtil.java

@Service
public class JwtUtil {

    @Value("${token.expiration.time}")
    private String tokenExpirationTime;

    private String SECRET_KEY = "some_secret";

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + Integer.parseInt(tokenExpirationTime)))
            .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

The issue I am having is that if I generate a token by calling service1/access-token, I can still call service2/test using the jwt, and vice versa. How can I restrict the user from accessing service2 with a valid service1 token? I believe I might have to create a second WebSecurityConfig class to handle service2, but I am not sure how it is done because the app won't compile when I try to add one.

Nuzzle answered 13/1, 2021 at 23:12 Comment(7)
I have not enough knowledge about this, as I'm still learning JWT myself, however reusing the AuthenticationManager seems a bit, I dunno, faulty to me.Heffner
You should use \@EnableResourceServer to control different token (with user) to access different url. And you can use \@EnableAuthorizationServer to define your authentication server or third-party authentication server provider ( such as hydra), in which different user has different priority with token. ResourceServer will check token via AuthenticationServer.Gambier
If you want to be responsible for the username/password, you need a database or somewhere else to store them. Storing users as properties, especially passwords is not acceptable, they need to be salted and hashed. But, really the problem you have is your design only supports 1 user, it could be extended to some more but not manyFenestrated
@Fenestrated Yep I am planning on moving them to a database in a few weeks. I just wanted to see how I can work with 2 users at the moment. Could you suggest a way that this can work even with this design?Nuzzle
@Gambier I am reading now about it but I am not 100% sure how to implement it in my case. Any advice on that by any chance?Nuzzle
My advice is based on oauth2 authentication. I think the modify will be huge. You should not use your current way to create and check token. Another simple way I think, maybe you try '@PreAuthorize(hasAnyAuthority('xxxx')' on each request url. Then you just change JwtRequestFilter in that you give different authorities according to user's role.Gambier
Have you already looked at this Spring Security's sample: github.com/spring-projects/spring-security-samples/tree/master/… it appears to be similar to what you are trying to achieve. You can see some additional detail in this SO answer: #64578581Electrostriction

© 2022 - 2024 — McMap. All rights reserved.