/*
* 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.text.MessageFormat;
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"));
doBasic2Oauth(context, role, auth[0], auth[1]);
} catch (RuntimeException e) {
handle400(context, e.getMessage());
}
}
private void doBasic2Oauth(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 = MessageFormat.format("User {0} does not have required role: {1}.", username, role);
log.error(message);
handle403(context, "insufficient_scope", message);
}
});
} else {
String message = tokenResult.cause().getMessage();
log.error("Access Token Error: {0}.", message);
handle401(context, "invalid_token", message);
}
});
}
private void handle400(RoutingContext context, String message) {
if (message != null) context.response().setStatusMessage(message);
context.fail(400);
}
private void handle401(RoutingContext context, String error, String message) {
String value = MessageFormat.format("Basic realm=\"{0}\" error=\"{1}\" error_message=\"{2}\"", "apiman-gw", error, message);
context.response().putHeader("WWW-Authenticate", value);
context.fail(401);
}
private void handle403(RoutingContext context, String error, String message) {
String value = MessageFormat.format("Basic realm=\"{0}\" error=\"{1}\" error_message=\"{2}\"", "apiman-gw", error, message);
context.response().putHeader("WWW-Authenticate", value);
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());
}
}