Issue
I am building a new API using Spring Boot. It will be exposed to external customers (outside the company network). This is done through a gateway (apigee).
Apigee provides an externally accessbile endpoint and secures it with Oauth2. When a client hits the external endpoint with valid credentails apigee forwards the request to the my API endpoint. Apigee also add a JWT in header, this will contain the API key used by client. In my application I have defined a filter (extends OncePerRequestFilter) that extracts the API key from the JWT by using the public key of the gateway. I am adding the filter using FilterRegistrationBean.
Now that I have the api-key in the filter I need to do two things:
- Hit an internal mySQL DB with the api-key and get various pieces of information about the caller (store this in ApiConsumer object).
- if the key is not found in DB then I want to return a 401.
I have seen many examples online that use Spring Security and code for AuthenticationManager, UserDetailsService etc for JWT validation. Do I have to do all that? Since I already have the api-key in my filter, is there a way I can make the DB call from the filter and verify if the key is present in the database? Then I can store the values in the apiConsumer.
Code for filter:
public class AuthFilter extends OncePerRequestFilter {
@Autowired
ApiConsumer aPiConsumer;
@Value("${jwt.pubkey}")
private String publickey;
@Value("${jwt.iss}")
private String iss;
@Value("${jwt.sub}")
private String sub;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new Exception("No JWT found");
}
String jwtToken = header.substring(7);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publickey.getBytes()));
PublicKey pubKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
@SuppressWarnings("rawtypes")
final JwsHeader jwsHeader = Jwts.parser().setSigningKey(pubKey).parseClaimsJws(jwtToken).getHeader();
String jwtType = jwsHeader.getType();
String jwtAlgorithm = jwsHeader.getAlgorithm();
if (!((jwtType.equals("JWT")) && (jwtAlgorithm.equals("RS256")))) {
throw new Exception("Invalid JWT Type or Algorithm");
}
final Claims claims = Jwts.parser().setSigningKey(pubKey).parseClaimsJws(jwtToken).getBody();
String issuer = claims.getIssuer();
String subject = claims.getSubject();
String apiKey = claims.get("apikey", String.class);
if (!((issuer.equals(iss)) && (subject.equals(sub)))) {
throw new Exception("Invalid JWT Issuer or Subject");
} else {
aPiConsumer.setApiKey(apikey);
//I have the api key. Now need to hit MySQL database to access this.
}
filterChain.doFilter(request, response);
}
}
Code for bean:
@Configuration
public class AuthConfig {
@Bean
public FilterRegistrationBean authorizationFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(authFilter());
registration.setName("authFilter");
return registration;
}
@Bean
public Filter authFilter() {
return new AuthFilter();
}
}
Solution
You autowired the apiConsumer
so you could as well autowire a service which reads the DB (or a repository), right?
As the commenter pointed out you could use some Spring built-in support for validating JWTs, then your filter will only have to read the DB and verify the API key. It's usually better to have such things handled by a library or framework, as the code tends to be more robust and tested. (E.g. your code will crash if you receive a malformed JWT, but in a proper Authorization header, etc.)
Answered By - Michal Trojanowski