feat(gateway-vertx) API auth: Add OAuth2, BASIC+Keycloak & improved BASIC.
ADD
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/api/auth/AuthFactory.java
+/*
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.apiman.gateway.platforms.vertx3.api.auth;
+
+import io.apiman.gateway.platforms.vertx3.common.config.VertxEngineConfig;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.AuthHandler;
+
+import java.util.Arrays;
+
+import org.apache.commons.lang3.EnumUtils;
+
+/**
+ * @author Marc Savy {@literal <marc@rhymewithgravy.com>}
+ */
+@SuppressWarnings("nls")
+public class AuthFactory {
+ public static enum AuthType {
+ NONE, BASIC, KEYCLOAK;
+
+ public static AuthType getType(String name) {
+ return EnumUtils.getEnum(AuthType.class, name.toUpperCase());
+ }
+
+ public static String all() {
+ return Arrays.toString(AuthType.values());
+ }
+ }
+
+ public static AuthHandler getAuth(Vertx vertx, Router router, VertxEngineConfig apimanConfig, JsonObject auth) {
+ String type = auth.getString("type", "NONE");
+ JsonObject authConfig = auth.getJsonObject("config", new JsonObject());
+
+ switch(AuthType.getType(type)) {
+ case BASIC:
+ return BasicAuth.create(authConfig);
+ case NONE:
+ return NoneAuth.create();
+ case KEYCLOAK:
+ return KeycloakOAuthFactory.create(vertx, router, apimanConfig, authConfig);
+ default:
+ return NoneAuth.create();
+ }
+ }
+
+}

ADD
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/api/auth/BasicAuth.java
+/*
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.apiman.gateway.platforms.vertx3.api.auth;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.AuthProvider;
+import io.vertx.ext.web.handler.AuthHandler;
+import io.vertx.ext.web.handler.impl.BasicAuthHandlerImpl;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * @author Marc Savy {@literal <marc@rhymewithgravy.com>}
+ */
+@SuppressWarnings("nls")
+public class BasicAuth extends BasicAuthHandlerImpl {
+
+ public BasicAuth(AuthProvider authProvider, String realm) {
+ super(authProvider, realm);
+ }
+
+ public static AuthHandler create(JsonObject apimanConfig) {
+ return new BasicAuth(authenticateBasic(apimanConfig), apimanConfig.getString("realm", "apiman-gateway"));
+ }
+
+ private static AuthProvider authenticateBasic(JsonObject apimanConfig) {
+ return (authInfo, resultHandler) -> {
+ String storedUsername = apimanConfig.getString("username");
+ String storedPassword = apimanConfig.getString("password");
+
+ if (storedUsername == null || storedPassword == null) {
+ resultHandler.handle(Future.failedFuture("Credentials not set in configuration."));
+ return;
+ }
+
+ String username = authInfo.getString("username");
+ String password = StringUtils.chomp(authInfo.getString("password"));
+
+ if (storedUsername.equals(username) && storedPassword.equals(password)) {
+ resultHandler.handle(Future.succeededFuture());
+ } else {
+ resultHandler.handle(Future.failedFuture("No such user, or password incorrect."));
+ }
+ };
+ }
+
+}

ADD
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/api/auth/KeycloakOAuthFactory.java
+/*
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.apiman.gateway.platforms.vertx3.api.auth;
+
+import io.apiman.common.util.Basic;
+import io.apiman.gateway.platforms.vertx3.common.config.VertxEngineConfig;
+import io.apiman.gateway.platforms.vertx3.verticles.ApiVerticle;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.logging.Logger;
+import io.vertx.core.logging.LoggerFactory;
+import io.vertx.ext.auth.oauth2.AccessToken;
+import io.vertx.ext.auth.oauth2.OAuth2Auth;
+import io.vertx.ext.auth.oauth2.OAuth2FlowType;
+import io.vertx.ext.auth.oauth2.providers.KeycloakAuth;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.AuthHandler;
+import io.vertx.ext.web.handler.OAuth2AuthHandler;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.commons.lang3.EnumUtils;
+
+/**
+ * @author Marc Savy {@literal <marc@rhymewithgravy.com>}
+ */
+@SuppressWarnings("nls")
+public class KeycloakOAuthFactory {
+ private static final Logger log = LoggerFactory.getLogger(KeycloakOAuthFactory.class);
+
+ public static AuthHandler create(Vertx vertx, Router router, VertxEngineConfig apimanConfig, JsonObject authConfig) {
+ OAuth2FlowType flowType = toEnum(authConfig.getString("flowType"));
+ String role = authConfig.getString("requiredRole");
+
+ Objects.requireNonNull(flowType, String.format("flowType must be specified and valid. Flows: %s.", Arrays.asList(OAuth2FlowType.values())));
+ Objects.requireNonNull(role, "requiredRole must be non-null.");
+
+ if (flowType != OAuth2FlowType.AUTH_CODE) {
+ return directGrant(vertx, apimanConfig, authConfig, flowType, role);
+ } else {
+ return standardAuth(vertx, router, apimanConfig, authConfig, flowType);
+ }
+ }
+
+ private static OAuth2AuthHandler standardAuth(Vertx vertx, Router router, VertxEngineConfig apimanConfig, JsonObject authConfig, OAuth2FlowType flowType) {
+ String proto = apimanConfig.isSSL() ? "https://" : "http://";
+ int port = apimanConfig.getPort(ApiVerticle.VERTICLE_TYPE);
+ String redirect = proto + apimanConfig.getHostname() + ":" + port; // Redirect back here to *after* auth.
+ // Set up KC OAuth2 Authentication
+ OAuth2AuthHandler auth = OAuth2AuthHandler.create(KeycloakAuth.create(vertx, flowType, authConfig), redirect);
+ // Callback can be anything (as long as it's not already used by something else).
+ auth.setupCallback(router.get("/callback"));
+ return auth;
+ }
+
+ private static AuthHandler directGrant(Vertx vertx, VertxEngineConfig apimanConfig, JsonObject authConfig,
+ OAuth2FlowType flowType, String role) {
+ return new AuthHandler() {
+
+ @Override
+ public void handle(RoutingContext context) {
+ try {
+ String[] auth = Basic.decodeWithScheme(context.request().getHeader("Authorization"));
+ doOauth2(context, role, auth[0], auth[1]);
+ } catch (RuntimeException e) {
+ context.response().setStatusMessage(e.getMessage());
+ context.fail(400);
+ }
+ }
+
+ private void doOauth2(RoutingContext context, String role, String username, String password) {
+ JsonObject params = new JsonObject()
+ .put("username", username)
+ .put("password", password);
+
+ OAuth2Auth oauth2 = KeycloakAuth.create(vertx, flowType, authConfig);
+ oauth2.getToken(params, tokenResult -> {
+ if (tokenResult.succeeded()) {
+ log.debug("OAuth2 Keycloak exchange succeeded.");
+ AccessToken token = tokenResult.result();
+ token.isAuthorised(role, res -> {
+ if (res.result()) {
+ context.next();
+ } else {
+ String message = "User {0} does not have required role: {1}.";
+ log.error(message, username, role);
+ context.response().setStatusMessage(message);
+ context.fail(403);
+ }
+ });
+ } else {
+ String message = tokenResult.cause().getMessage();
+ log.error("Access Token Error: {0}.", message);
+ context.response().setStatusMessage(message);
+ context.fail(403);
+ }
+ });
+ }
+
+ @Override
+ public AuthHandler addAuthority(String authority) {
+ return this;
+ }
+
+ @Override
+ public AuthHandler addAuthorities(Set<String> authorities) {
+ return this;
+ }
+ };
+ }
+
+ private static OAuth2FlowType toEnum(String flowType) {
+ return EnumUtils.getEnum(OAuth2FlowType.class, flowType.toUpperCase());
+ }
+
+}

ADD
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/api/auth/NoneAuth.java
+/*
+ * Copyright 2017 JBoss Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.apiman.gateway.platforms.vertx3.api.auth;
+
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.AuthHandler;
+
+import java.util.Set;
+
+/**
+ * @author Marc Savy {@literal <marc@rhymewithgravy.com>}
+ */
+public class NoneAuth implements AuthHandler {
+
+ @Override
+ public void handle(RoutingContext context) {
+ context.response().setStatusMessage("WARNING: NoneAuth Auth selected. Always returns 401."); //$NON-NLS-1$
+ context.fail(401);
+ }
+
+ @Override
+ public AuthHandler addAuthority(String authority) {
+ return this;
+ }
+
+ @Override
+ public AuthHandler addAuthorities(Set<String> authorities) {
+ return this;
+ }
+
+ public static AuthHandler create() {
+ return new NoneAuth();
+ }
+
+}

MODIFY
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/common/config/VertxEngineConfig.java
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/common/config/VertxEngineConfig.java
@@ -72,8 +72,6 @@ public class VertxEngineConfig implements IEngineConfig {
private static final String API_AUTH = "auth";
private static final String API_PASSWORD = "password";
- private static final String API_REQUIRED = "required";
- private static final String API_REALM = "realm";
private static final String GATEWAY_REGISTRY_PREFIX = "registry";
private static final String GATEWAY_ENCRYPTER_PREFIX = "encrypter";
@@ -92,7 +90,6 @@ public class VertxEngineConfig implements IEngineConfig {
private static final String SSL_PATH = "path";
private JsonObject config;
- private HashMap<String, String> basicAuthMap = new HashMap<>();
public VertxEngineConfig(JsonObject config) {
this.config = config;
@@ -228,14 +225,6 @@ public class VertxEngineConfig implements IEngineConfig {
return Collections.emptyList();
}
- public Boolean isAuthenticationEnabled() {
- return config.getJsonObject(API_AUTH).getString(API_REQUIRED) != null;
- }
-
- public String getRealm() {
- return config.getJsonObject(API_AUTH).getString(API_REALM);
- }
-
public String getHostname() {
return stringConfigWithDefault(GATEWAY_HOSTNAME, "localhost");
}
@@ -248,17 +237,40 @@ public class VertxEngineConfig implements IEngineConfig {
return config.getBoolean(GATEWAY_PREFER_SECURE);
}
- public Map<String, String> getBasicAuthCredentials() {
- if (!basicAuthMap.isEmpty())
- return basicAuthMap;
+ public int getPort(String name) {
+ return getVerticleConfig(name).getInteger(VERTICLE_PORT, -1);
+ }
+
+ public int getPort(VerticleType verticleType) {
+ return getPort(verticleType.name());
+ }
+
+ public int getVerticleCount(VerticleType verticleType) {
+ return getVerticleConfig(verticleType.name()).getInteger(VERTICLE_COUNT);
+ }
+
+ public boolean isSSL() {
+ return config.containsKey(SSL);
+ }
- JsonObject pairs = config.getJsonObject(API_AUTH).getJsonObject("basic");
+ public String getKeyStore() {
+ return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_KEYSTORE, new JsonObject()).getString(SSL_PATH);
+ }
- for (String username : pairs.fieldNames()) {
- basicAuthMap.put(username, pairs.getString(username));
+ public String getKeyStorePassword() {
+ return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_KEYSTORE, new JsonObject()).getString(API_PASSWORD);
+ }
+
+ public String getTrustStore() {
+ return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_TRUSTSTORE, new JsonObject()).getString(SSL_PATH);
+ }
+
+ public String getTrustStorePassword() {
+ return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_TRUSTSTORE, new JsonObject()).getString(API_PASSWORD);
}
- return basicAuthMap;
+ public JsonObject getAuth() {
+ return config.getJsonObject(API_AUTH, new JsonObject());
}
protected Map<String, String> toFlatStringMap(JsonObject jsonObject) {
@@ -356,37 +368,7 @@ public class VertxEngineConfig implements IEngineConfig {
return config.getJsonObject(VERTICLES).getJsonObject(verticleType.toLowerCase());
}
- public int getPort(String name) {
- return getVerticleConfig(name).getInteger(VERTICLE_PORT);
- }
-
- public int getPort(VerticleType verticleType) {
- return getPort(verticleType.name());
- }
-
- public int getVerticleCount(VerticleType verticleType) {
- return getVerticleConfig(verticleType.name()).getInteger(VERTICLE_COUNT);
- }
-
- public boolean isSSL() {
- return config.containsKey(SSL);
- }
-
- public String getKeyStore() {
- return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_KEYSTORE, new JsonObject()).getString(SSL_PATH);
- }
-
- public String getKeyStorePassword() {
- return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_KEYSTORE, new JsonObject()).getString(API_PASSWORD);
- }
- public String getTrustStore() {
- return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_TRUSTSTORE, new JsonObject()).getString(SSL_PATH);
- }
-
- public String getTrustStorePassword() {
- return config.getJsonObject(SSL, new JsonObject()).getJsonObject(SSL_TRUSTSTORE, new JsonObject()).getString(API_PASSWORD);
- }
/**
* Gets all properties in the engine configuration that are prefixed

MODIFY
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/verticles/ApiVerticle.java
gateway/platforms/vertx3/vertx3/src/main/java/io/apiman/gateway/platforms/vertx3/verticles/ApiVerticle.java
@@ -19,19 +19,13 @@ import io.apiman.gateway.platforms.vertx3.api.ApiResourceImpl;
import io.apiman.gateway.platforms.vertx3.api.ClientResourceImpl;
import io.apiman.gateway.platforms.vertx3.api.IRouteBuilder;
import io.apiman.gateway.platforms.vertx3.api.SystemResourceImpl;
+import io.apiman.gateway.platforms.vertx3.api.auth.AuthFactory;
import io.apiman.gateway.platforms.vertx3.common.verticles.VerticleType;
-import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
-import io.vertx.core.Handler;
-import io.vertx.core.json.JsonObject;
-import io.vertx.ext.auth.User;
+import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.net.JksOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.AuthHandler;
-import io.vertx.ext.web.handler.BasicAuthHandler;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang3.StringUtils;
/**
* API verticle provides the Gateway API RESTful API. Config is validated and pushed into the registry
@@ -51,18 +45,37 @@ public class ApiVerticle extends ApimanVerticleWithEngine {
IRouteBuilder apiResource = new ApiResourceImpl(apimanConfig, engine);
IRouteBuilder systemResource = new SystemResourceImpl(apimanConfig, engine);
- Router router = Router.router(vertx);
+ Router router = Router.router(vertx)
+ .exceptionHandler(log::error);
- if (apimanConfig.isAuthenticationEnabled()) {
- AuthHandler basicAuthHandler = BasicAuthHandler.create(this::authenticateBasic, apimanConfig.getRealm());
- router.route("/*").handler(basicAuthHandler);
- }
+ AuthHandler handler = AuthFactory.getAuth(vertx, router, apimanConfig, apimanConfig.getAuth());
+
+ router.route("/*").handler(handler);
clientResource.buildRoutes(router);
apiResource.buildRoutes(router);
systemResource.buildRoutes(router);
- vertx.createHttpServer()
+ HttpServerOptions httpOptions = new HttpServerOptions()
+ .setHost(apimanConfig.getHostname());
+
+ if (apimanConfig.isSSL()) {
+ httpOptions.setSsl(true)
+ .setKeyStoreOptions(
+ new JksOptions()
+ .setPath(apimanConfig.getKeyStore())
+ .setPassword(apimanConfig.getKeyStorePassword())
+ )
+ .setTrustStoreOptions(
+ new JksOptions()
+ .setPath(apimanConfig.getTrustStore())
+ .setPassword(apimanConfig.getTrustStorePassword())
+ );
+ } else {
+ log.warn("API is running in plaintext mode. Enable SSL in config for production deployments.");
+ }
+
+ vertx.createHttpServer(httpOptions)
.requestHandler(router::accept)
.listen(apimanConfig.getPort(VERTICLE_TYPE));
}
@@ -71,16 +84,4 @@ public class ApiVerticle extends ApimanVerticleWithEngine {
public VerticleType verticleType() {
return VERTICLE_TYPE;
}
-
- public void authenticateBasic(JsonObject authInfo, Handler<AsyncResult<User>> resultHandler) {
- String username = authInfo.getString("username");
- String password = StringUtils.chomp(Base64.encodeBase64String(DigestUtils.sha256(authInfo.getString("password")))); // Chomp, Digest, Base64Encode
- String storedPassword = apimanConfig.getBasicAuthCredentials().get(username);
-
- if (storedPassword != null && password.equals(storedPassword)) {
- resultHandler.handle(Future.<User>succeededFuture(null));
- } else {
- resultHandler.handle(Future.<User>failedFuture("Not such user, or password is incorrect."));
- }
- }
}

MODIFY
gateway/test/src/test/resources/vertx3/conf-api-to-file.json
gateway/test/src/test/resources/vertx3/conf-api-to-file.json
// API authentication; presently just BASIC auth.
"auth": {
- "required": "basic",
+ "type": "basic",
+ "config": {
"realm": "apiman-gateway",
- "basic": { // Password is SHA256 digest encoded as Base64.
- "admin" : "jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="
+ "username" : "admin",
+ "password": "admin"
}
},

MODIFY
gateway/test/src/test/resources/vertx3/conf-es.json
gateway/test/src/test/resources/vertx3/conf-es.json
// API authentication; presently just BASIC auth.
"auth": {
- "required": "basic",
+ "type": "basic",
+ "config": {
"realm": "apiman-gateway",
- "basic": { // Password is SHA256 digest encoded as Base64.
- "admin" : "jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="
+ "username" : "admin",
+ "password": "admin"
}
},

MODIFY
gateway/test/src/test/resources/vertx3/conf-file.json
gateway/test/src/test/resources/vertx3/conf-file.json
// API authentication; presently just BASIC auth.
"auth": {
- "required": "basic",
+ "type": "basic",
+ "config": {
"realm": "apiman-gateway",
- "basic": { // Password is SHA256 digest encoded as Base64.
- "admin" : "jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="
+ "username" : "admin",
+ "password": "admin"
}
},

MODIFY
gateway/test/src/test/resources/vertx3/conf-mem.json
gateway/test/src/test/resources/vertx3/conf-mem.json
// API authentication; presently just BASIC auth.
"auth": {
- "required": "basic",
+ "type": "basic",
+ "config": {
"realm": "apiman-gateway",
- "basic": { // Password is SHA256 digest encoded as Base64.
- "admin" : "jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="
+ "username" : "admin",
+ "password": "admin"
}
},

MODIFY
gateway/test/src/test/resources/vertx3/conf-shared-global-data.json
gateway/test/src/test/resources/vertx3/conf-shared-global-data.json
// API authentication; presently just BASIC auth.
"auth": {
- "required": "basic",
+ "type": "basic",
+ "config": {
"realm": "apiman-gateway",
- "basic": { // Password is SHA256 digest encoded as Base64.
- "admin" : "jGl25bVBBBW96Qi9Te4V37Fnqchz/Eu4qB9vKrRIqRg="
+ "username" : "admin",
+ "password": "admin"
}
},

feat(gateway-vertx) API auth: Add OAuth2, BASIC+Keycloak & improved BASIC.