I inherited a half-written Spring Boot REST service that is using Spring Sec to implement JWT-based API authentication. Gradle security-related dependencies are:
'org.springframework.security:spring-security-jwt:1.0.9.RELEASE'
'org.springframework.security.oauth:spring-security-oauth2:2.2.1.RELEASE'
'io.jsonwebtoken:jjwt:0.9.0'
'org.springframework.boot:spring-boot-starter-security'
This app uses Spring Sec filters to implement the entire auth solution, and I'm trying to wrap my head around how it works, and for the life of me can't make sense of a few critical things :-/
Here's the code:
public class MyAppAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public MyAppAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET.getBytes())
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
public class MyAppAuthorizationFilter extends BasicAuthenticationFilter {
public MyAppAuthorizationFilter(AuthenticationManager authManager) {
super(authManager);
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(req);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private AccountDAO accountDAO;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountDAO.findByUsername(username);
if(account == null) {
throw new UsernameNotFoundException(username);
}
return new User(account.username, account.password, []);
}
}
What I'm not understanding is:
- Can I assume that Spring Security automagically positions these filters in the correct sequence? That is: the
MyAppAuthenticationFilter
always gets called before theMyAppAuthorizationFilter
? - I'm really confused by the
authenticationManager.authenticate(...)
call insideMyAppAuthenticationFilter#attemptAuthentication
. How arecreds.getUsename()
andcred.getPassword()
compared to user information stored in a database (or LDAP or anywhere else)? How does this mechanism relate toUserDetailsServiceImpl#loadByUsername(String)
? - All of the logic in
MyAppAuthorizationFilter#doFilterInternal
doesn't make sense to me. To me, I read it as: check to see if there is a JWT token header on the request. If there isn't, then go ahead and make the request any way (!!!!). If there is, then go ahead and check that the JWT has a valid user as its subject. Shouldn't we be blocking the request if there's no JWT header on the request?