I have multiple applications(each can be considered a resource server) and single authorization server. The authorization server issues a JWT
on successful authentication. In this RFC, I read about aud
claim and it is good way to notify a client that a token is not meant for it i.e. the recipient can check the aud
claim in JWT and discard the JWT
not meant for it.
However, what I am trying to find out is how to maintain a separate key/secret
for each Resource server hidden from other resource servers but known to authorization server.
The reason I want to keep key/secret at the resource server is because I want to avoid sending a request to authorization server to validate token every time.(performance hit).
And the reason I want to keep a separate key/secret is security.
I am using this jsonWebToken library.
Is it possible to do this?
First of all you could use a cache to avoid checking authorization server every time. You can cache it for the token lifetime.
Then you could change to RS256 and expose the public key on an endpoint that can be reached by your resources servers that will be cached on startup and do the checking in the resources servers. But for performance it will also be good to cache the result of the validation.
Quick example on how to handle signing and verification:
public class JwtFactory {
private Map<String, Key> keys = new HashMap<>();
public JwtFactory(String keyStore, String keyStorePassword, Map<String, String> resourcesPassword) {
try {
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (InputStream is = Files.newInputStream(Paths.get("jwt.pkcs12"))) {
keystore.load(is, keyStorePassword.toCharArray());
}
for (Map.Entry<String, String> resource : resourcesPassword.entrySet()) {
keys.put(resource.getKey(), keystore.getKey(resource.getKey(), resource.getValue().toCharArray()));
}
} catch (Exception e) {
throw new RuntimeException("Unable to load key", e);
}
}
public String createToken(String resourceId, String subject, String id, Map<String, Object> claims) {
return Jwts.builder()
.setSubject(subject)
.setId(id)
.setIssuedAt(Date.from(Instant.ofEpochSecond(System.currentTimeMillis() - 3600)))
.setExpiration(Date.from(Instant.ofEpochSecond(System.currentTimeMillis() + 3600)))
.addClaims(claims)
.signWith(SignatureAlgorithm.RS256, keys.get(resourceId))
.compact();
}
public static void main(String[] args) throws Exception {
final Map<String, String> resources = new HashMap<>();
resources.put("resource1", "password");
resources.put("resource2", "password");
final Map<String, Object> claims = new HashMap<>();
claims.put("name", "John Doe");
claims.put("admin", true);
JwtFactory factory = new JwtFactory("jwt.pkcs12", "password", resources);
String token1 = factory.createToken("resource1", "1234567890", "a8070da2-3497-4a51-a932-daa9ae53bddd", claims);
String token2 = factory.createToken("resource2", "1234567890", "a8070da2-3497-4a51-a932-daa9ae53bddd", claims);
System.out.println(token1);
System.out.println(token2);
final String resource1Public = new String(Files.readAllBytes(Paths.get("resource1.pem")), StandardCharsets.ISO_8859_1)
.replaceAll("-----BEGIN PUBLIC KEY-----\n", "")
.replaceAll("-----END PUBLIC KEY-----", "");
final X509EncodedKeySpec specKey1 = new X509EncodedKeySpec(Base64.decodeBase64(resource1Public.getBytes(StandardCharsets.ISO_8859_1)));
Jwt jwt = Jwts.parser().setSigningKey(KeyFactory.getInstance("RSA").generatePublic(specKey1)).parse(token1);
System.out.println("Validation Ok with resource 1");
System.out.println(jwt);
try {
Jwts.parser().setSigningKey(KeyFactory.getInstance("RSA").generatePublic(specKey1)).parse(token2);
} catch (Exception e) {
System.out.println("Validation fail with resource 2");
}
}
}
generate a keypair:
keytool -genkeypair -alias resource1 -keyalg RSA -keypass password -keystore jwt.pkcs12 -storepass password
Extract public key:
keytool -list -rfc -keystore jwt.pkcs12 -alias resource1 | openssl x509 -inform pem -pubkey -noout