package io.vertx.ext.auth.jwt.impl;
import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.FileSystemException;
import io.vertx.core.json.Json;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.JWTOptions;
import io.vertx.ext.auth.KeyStoreOptions;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.User;
import io.vertx.ext.auth.authentication.Credentials;
import io.vertx.ext.auth.authentication.TokenCredentials;
import io.vertx.ext.auth.authorization.PermissionBasedAuthorization;
import io.vertx.ext.auth.impl.jose.JWK;
import io.vertx.ext.auth.impl.jose.JWT;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class JWTAuthProviderImpl implements JWTAuth {
private static final JsonArray EMPTY_ARRAY = new JsonArray();
private final JWT jwt = new JWT();
private final String permissionsClaimKey;
private final JWTOptions jwtOptions;
public JWTAuthProviderImpl(Vertx vertx, JWTAuthOptions config) {
this.permissionsClaimKey = config.getPermissionsClaimKey();
this.jwtOptions = config.getJWTOptions();
final KeyStoreOptions keyStore = config.getKeyStore();
try {
if (keyStore != null) {
final KeyStore ks;
if (keyStore.getProvider() == null) {
ks = KeyStore.getInstance(keyStore.getType());
} else {
ks = KeyStore.getInstance(keyStore.getType(), keyStore.getProvider());
}
synchronized (JWTAuthProviderImpl.class) {
final Buffer keystore = vertx.fileSystem().readFileBlocking(keyStore.getPath());
try (InputStream in = new ByteArrayInputStream(keystore.getBytes())) {
ks.load(in, keyStore.getPassword().toCharArray());
}
}
for (JWK key : JWK.load(ks, keyStore.getPassword(), keyStore.getPasswordProtection())) {
jwt.addJWK(key);
}
}
final List<PubSecKeyOptions> keys = config.getPubSecKeys();
if (keys != null) {
for (PubSecKeyOptions pubSecKey : config.getPubSecKeys()) {
jwt.addJWK(new JWK(pubSecKey));
}
}
final List<JsonObject> jwks = config.getJwks();
if (jwks != null) {
for (JsonObject jwk : jwks) {
this.jwt.addJWK(new JWK(jwk));
}
}
} catch (KeyStoreException | IOException | FileSystemException | CertificateException | NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException(e);
}
}
@Override
public void authenticate(JsonObject authInfo, Handler<AsyncResult<User>> resultHandler) {
authenticate(new TokenCredentials(authInfo.getString("jwt")), resultHandler);
}
@Override
public void authenticate(Credentials credentials, Handler<AsyncResult<User>> resultHandler) {
try {
TokenCredentials authInfo = (TokenCredentials) credentials;
authInfo.checkValid(null);
final JsonObject payload = jwt.decode(authInfo.getToken());
if (jwtOptions.getAudience() != null) {
JsonArray target;
if (payload.getValue("aud") instanceof String) {
target = new JsonArray().add(payload.getValue("aud", ""));
} else {
target = payload.getJsonArray("aud", EMPTY_ARRAY);
}
if (Collections.disjoint(jwtOptions.getAudience(), target.getList())) {
resultHandler.handle(Future.failedFuture("Invalid JWT audience. expected: " + Json.encode(jwtOptions.getAudience())));
return;
}
}
if (jwtOptions.getIssuer() != null) {
if (!jwtOptions.getIssuer().equals(payload.getString("iss"))) {
resultHandler.handle(Future.failedFuture("Invalid JWT issuer"));
return;
}
}
if (!jwt.isScopeGranted(payload, jwtOptions)) {
resultHandler.handle(Future.failedFuture("Invalid JWT token: missing required scopes."));
return;
}
final User user = createUser(authInfo.getToken(), payload, permissionsClaimKey);
if (user.expired(jwtOptions.getLeeway())) {
if (!jwtOptions.isIgnoreExpiration()) {
resultHandler.handle(Future.failedFuture("Invalid JWT token: missing required scopes."));
return;
}
}
resultHandler.handle(Future.succeededFuture(user));
} catch (RuntimeException e) {
resultHandler.handle(Future.failedFuture(e));
}
}
@Override
public String generateToken(JsonObject claims, final JWTOptions options) {
final JsonObject _claims = claims.copy();
if (options.getPermissions() != null && !_claims.containsKey(permissionsClaimKey)) {
_claims.put(permissionsClaimKey, new JsonArray(options.getPermissions()));
}
return jwt.sign(_claims, options);
}
private static JsonArray getJsonPermissions(JsonObject jwtToken, String permissionsClaimKey) {
if (permissionsClaimKey.contains("/")) {
return getNestedJsonValue(jwtToken, permissionsClaimKey);
}
return jwtToken.getJsonArray(permissionsClaimKey, null);
}
private static final Collection<String> SPECIAL_KEYS = Arrays.asList("access_token", "exp", "iat", "nbf");
@Deprecated
private User createUser(String accessToken, JsonObject jwtToken, String permissionsClaimKey) {
User result = User.fromToken(accessToken);
result.attributes()
.put("accessToken", jwtToken);
copyProperties(jwtToken, result.attributes(), "exp", "iat", "nbf", "sub");
for (String key : jwtToken.fieldNames()) {
if (!SPECIAL_KEYS.contains(key)) {
result.principal().put(key, jwtToken.getValue(key));
}
}
result.attributes()
.put("rootClaim", "accessToken");
JsonArray jsonPermissions = getJsonPermissions(jwtToken, permissionsClaimKey);
if (jsonPermissions != null) {
for (Object item : jsonPermissions) {
if (item instanceof String) {
String permission = (String) item;
result.authorizations().add("jwt-authentication", PermissionBasedAuthorization.create(permission));
}
}
}
return result;
}
private static void copyProperties(JsonObject source, JsonObject target, String... keys) {
if (source != null && target != null) {
for (String key : keys) {
if (source.containsKey(key) && !target.containsKey(key)) {
target.put(key, source.getValue(key));
}
}
}
}
private static JsonArray getNestedJsonValue(JsonObject jwtToken, String permissionsClaimKey) {
String[] keys = permissionsClaimKey.split("/");
JsonObject obj = null;
for (int i = 0; i < keys.length; i++) {
if (i == 0) {
obj = jwtToken.getJsonObject(keys[i]);
} else if (i == keys.length - 1) {
if (obj != null) {
return obj.getJsonArray(keys[i]);
}
} else {
if (obj != null) {
obj = obj.getJsonObject(keys[i]);
}
}
}
return null;
}
}