diff --git a/openapi-security/pom.xml b/openapi-security/pom.xml index 2f5d38a6..64eb57b2 100644 --- a/openapi-security/pom.xml +++ b/openapi-security/pom.xml @@ -54,6 +54,18 @@ com.networknt status + + com.networknt + basic-auth + + + com.networknt + api-key + + + com.networknt + server + com.networknt json-overlay diff --git a/openapi-security/src/main/java/com/networknt/openapi/JwtVerifyHandler.java b/openapi-security/src/main/java/com/networknt/openapi/JwtVerifyHandler.java index 6b4cbe18..b65a947f 100644 --- a/openapi-security/src/main/java/com/networknt/openapi/JwtVerifyHandler.java +++ b/openapi-security/src/main/java/com/networknt/openapi/JwtVerifyHandler.java @@ -105,11 +105,19 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { logger.debug("JwtVerifyHandler.handleRequest ends."); return; } + // only UnifiedSecurityHandler will have the jwkServiceIds as the third parameter. + if(handleJwt(exchange, null, reqPath, null)) { + if(logger.isDebugEnabled()) logger.debug("JwtVerifyHandler.handleRequest ends."); + Handler.next(exchange, next); + } + } + + public boolean handleJwt(HttpServerExchange exchange, String pathPrefix, String reqPath, List jwkServiceIds) throws Exception { Map auditInfo = null; HeaderMap headerMap = exchange.getRequestHeaders(); String authorization = headerMap.getFirst(Headers.AUTHORIZATION); - if (logger.isTraceEnabled() && authorization != null) + if (logger.isTraceEnabled() && authorization != null && authorization.length() > 10) logger.trace("Authorization header = " + authorization.substring(0, 10)); authorization = this.getScopeToken(authorization, headerMap); @@ -125,7 +133,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { try { - JwtClaims claims = jwtVerifier.verifyJwt(jwt, ignoreExpiry, true, reqPath); + JwtClaims claims = jwtVerifier.verifyJwt(jwt, ignoreExpiry, true, pathPrefix, reqPath, jwkServiceIds); if (logger.isTraceEnabled()) logger.trace("claims = " + claims.toJson()); @@ -156,7 +164,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { if (!config.isEnableH2c() && this.checkForH2CRequest(headerMap)) { setExchangeStatus(exchange, STATUS_METHOD_NOT_ALLOWED); if (logger.isDebugEnabled()) logger.debug("JwtVerifyHandler.handleRequest ends with an error."); - return; + return false; } String callerId = headerMap.getFirst(HttpStringConstants.CALLER_ID); @@ -178,7 +186,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { } else { // this will return an error message to the client. } - return; + return false; } /* validate scope from operation */ @@ -186,11 +194,11 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { String scopeJwt = JwtVerifier.getJwtFromAuthorization(scopeHeader); List secondaryScopes = new ArrayList<>(); - if(!this.hasValidSecondaryScopes(exchange, scopeJwt, secondaryScopes, ignoreExpiry, reqPath, auditInfo)) { - return; + if(!this.hasValidSecondaryScopes(exchange, scopeJwt, secondaryScopes, ignoreExpiry, pathPrefix, reqPath, jwkServiceIds, auditInfo)) { + return false; } if(!this.hasValidScope(exchange, scopeHeader, secondaryScopes, claims, operation)) { - return; + return false; } } if (logger.isTraceEnabled()) @@ -199,7 +207,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { if (logger.isDebugEnabled()) logger.debug("JwtVerifyHandler.handleRequest ends."); - Handler.next(exchange, next); + return true; } catch (InvalidJwtException e) { // only log it and unauthorized is returned. @@ -227,8 +235,8 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { setExchangeStatus(exchange, STATUS_MISSING_AUTH_TOKEN); exchange.endExchange(); } + return true; } - /** * Get authToken from X-Scope-Token header. * This covers situations where there is a secondary auth token. @@ -319,13 +327,13 @@ protected Operation getOperation(HttpServerExchange exchange, OpenApiOperation o * @param reqPath - the request path as string * @return - return true if the secondary scopes are valid or if there are no secondary scopes. */ - protected boolean hasValidSecondaryScopes(HttpServerExchange exchange, String scopeJwt, List secondaryScopes, boolean ignoreExpiry, String reqPath, Map auditInfo) { + protected boolean hasValidSecondaryScopes(HttpServerExchange exchange, String scopeJwt, List secondaryScopes, boolean ignoreExpiry, String pathPrefix, String reqPath, List jwkServiceIds, Map auditInfo) { if (scopeJwt != null) { if (logger.isTraceEnabled()) logger.trace("start verifying scope token = " + scopeJwt.substring(0, 10)); try { - JwtClaims scopeClaims = jwtVerifier.verifyJwt(scopeJwt, ignoreExpiry, true, reqPath); + JwtClaims scopeClaims = jwtVerifier.verifyJwt(scopeJwt, ignoreExpiry, true, pathPrefix, reqPath, jwkServiceIds); Object scopeClaim = scopeClaims.getClaimValue(Constants.SCOPE_STRING); if (scopeClaim instanceof String) { diff --git a/openapi-security/src/main/java/com/networknt/openapi/UnifiedPathPrefixAuth.java b/openapi-security/src/main/java/com/networknt/openapi/UnifiedPathPrefixAuth.java new file mode 100644 index 00000000..1308ab83 --- /dev/null +++ b/openapi-security/src/main/java/com/networknt/openapi/UnifiedPathPrefixAuth.java @@ -0,0 +1,51 @@ +package com.networknt.openapi; + +import java.util.List; + +public class UnifiedPathPrefixAuth { + String pathPrefix; + boolean basic; + boolean jwt; + boolean apikey; + List jwkServiceIds; + + public String getPathPrefix() { + return pathPrefix; + } + + public void setPathPrefix(String pathPrefix) { + this.pathPrefix = pathPrefix; + } + + public boolean isBasic() { + return basic; + } + + public void setBasic(boolean basic) { + this.basic = basic; + } + + public boolean isJwt() { + return jwt; + } + + public void setJwt(boolean jwt) { + this.jwt = jwt; + } + + public boolean isApikey() { + return apikey; + } + + public void setApikey(boolean apikey) { + this.apikey = apikey; + } + + public List getJwkServiceIds() { + return jwkServiceIds; + } + + public void setJwkServiceIds(List jwkServiceIds) { + this.jwkServiceIds = jwkServiceIds; + } +} diff --git a/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityConfig.java b/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityConfig.java new file mode 100644 index 00000000..a7a66024 --- /dev/null +++ b/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityConfig.java @@ -0,0 +1,177 @@ +package com.networknt.openapi; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.networknt.config.Config; +import com.networknt.config.ConfigException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class UnifiedSecurityConfig { + private static final Logger logger = LoggerFactory.getLogger(UnifiedSecurityConfig.class); + public static final String CONFIG_NAME = "unified-security"; + public static final String ENABLED = "enabled"; + public static final String ANONYMOUS_PREFIXES = "anonymousPrefixes"; + public static final String PATH_PREFIX_AUTHS = "pathPrefixAuths"; + public static final String PREFIX = "prefix"; + public static final String BASIC = "basic"; + public static final String JWT = "jwt"; + public static final String APIKEY = "apikey"; + public static final String JWK_SERVICE_IDS = "jwkServiceIds"; + + boolean enabled; + List anonymousPrefixes; + List pathPrefixAuths; + + private Config config; + private Map mappedConfig; + + private UnifiedSecurityConfig() { + this(CONFIG_NAME); + } + + /** + * Please note that this constructor is only for testing to load different config files + * to test different configurations. + * @param configName String + */ + private UnifiedSecurityConfig(String configName) { + config = Config.getInstance(); + mappedConfig = config.getJsonMapConfigNoCache(configName); + setConfigData(); + setConfigList(); + } + public static UnifiedSecurityConfig load() { + return new UnifiedSecurityConfig(); + } + + public static UnifiedSecurityConfig load(String configName) { + return new UnifiedSecurityConfig(configName); + } + + void reload() { + mappedConfig = config.getJsonMapConfigNoCache(CONFIG_NAME); + setConfigData(); + setConfigList(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getAnonymousPrefixes() { + return anonymousPrefixes; + } + + public void setAnonymousPrefixes(List anonymousPrefixes) { + this.anonymousPrefixes = anonymousPrefixes; + } + + public List getPathPrefixAuths() { + return pathPrefixAuths; + } + + public void setPathPrefixAuths(List pathPrefixAuths) { + this.pathPrefixAuths = pathPrefixAuths; + } + + private void setConfigData() { + Object object = mappedConfig.get(ENABLED); + if(object != null && (Boolean) object) { + setEnabled(true); + } + } + + private void setConfigList() { + // anonymous prefixes + if (mappedConfig != null && mappedConfig.get(ANONYMOUS_PREFIXES) != null) { + Object object = mappedConfig.get(ANONYMOUS_PREFIXES); + anonymousPrefixes = new ArrayList<>(); + if(object instanceof String) { + String s = (String)object; + s = s.trim(); + if(logger.isTraceEnabled()) logger.trace("s = " + s); + if(s.startsWith("[")) { + // json format + try { + anonymousPrefixes = Config.getInstance().getMapper().readValue(s, new TypeReference>() {}); + } catch (Exception e) { + throw new ConfigException("could not parse the anonymousPrefixes json with a list of strings."); + } + } else { + // comma separated + anonymousPrefixes = Arrays.asList(s.split("\\s*,\\s*")); + } + } else if (object instanceof List) { + List prefixes = (List)object; + prefixes.forEach(item -> { + anonymousPrefixes.add((String)item); + }); + } else { + throw new ConfigException("anonymousPrefixes must be a string or a list of strings."); + } + } + + // path prefix auth mapping + if (mappedConfig.get(PATH_PREFIX_AUTHS) != null) { + Object object = mappedConfig.get(PATH_PREFIX_AUTHS); + pathPrefixAuths = new ArrayList<>(); + if(object instanceof String) { + String s = (String)object; + s = s.trim(); + if(logger.isTraceEnabled()) logger.trace("pathPrefixAuth s = " + s); + if(s.startsWith("[")) { + // json format + try { + pathPrefixAuths = Config.getInstance().getMapper().readValue(s, new TypeReference>() {}); + } catch (Exception e) { + throw new ConfigException("could not parse the pathPrefixAuths json with a list of string and object."); + } + } else { + throw new ConfigException("pathPrefixAuths must be a list of string object map."); + } + } else if (object instanceof List) { + // the object is a list of map, we need convert it to PathPrefixAuth object. + List> values = (List>)object; + for(Map value: values) { + UnifiedPathPrefixAuth unifiedPathPrefixAuth = new UnifiedPathPrefixAuth(); + + unifiedPathPrefixAuth.setPathPrefix((String)value.get(PREFIX)); + unifiedPathPrefixAuth.setBasic(value.get(BASIC) == null ? false : (Boolean)value.get(BASIC)); + unifiedPathPrefixAuth.setJwt(value.get(JWT) == null ? false : (Boolean)value.get(JWT)); + unifiedPathPrefixAuth.setApikey(value.get(APIKEY) == null ? false : (Boolean)value.get(APIKEY)); + Object ids = value.get(JWK_SERVICE_IDS); + if(ids instanceof String) { + String s = (String)value.get(JWK_SERVICE_IDS); + if(s.startsWith("[")) { + // json format + try { + unifiedPathPrefixAuth.setJwkServiceIds(Config.getInstance().getMapper().readValue(s, new TypeReference>() {})); + } catch (Exception e) { + throw new ConfigException("could not parse the jwkServiceIds json with a list of strings."); + } + } else { + // comma separated + unifiedPathPrefixAuth.setJwkServiceIds(Arrays.asList(s.split("\\s*,\\s*"))); + } + } else if(ids instanceof List ) { + // it must be a json array. + unifiedPathPrefixAuth.setJwkServiceIds((List)ids); + } + pathPrefixAuths.add(unifiedPathPrefixAuth); + } + } else { + throw new ConfigException("pathPrefixAuth must be a list of string object map."); + } + } + } + +} diff --git a/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityHandler.java b/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityHandler.java new file mode 100644 index 00000000..a5213eb0 --- /dev/null +++ b/openapi-security/src/main/java/com/networknt/openapi/UnifiedSecurityHandler.java @@ -0,0 +1,185 @@ +package com.networknt.openapi; + +import com.networknt.apikey.ApiKeyHandler; +import com.networknt.basicauth.BasicAuthHandler; +import com.networknt.config.Config; +import com.networknt.handler.Handler; +import com.networknt.handler.MiddlewareHandler; +import com.networknt.utility.ModuleRegistry; +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +/** + * This is a security handler that combines Anonymous, ApiKey, Basic and OAuth together to avoid all of them + * to be wired in the request/response chain and skip some of them based on the request path. It allows one + * path to choose several security handlers at the same time. In most cases, this handler will only be used + * in a shard light-gateway instance. + * + * @author Steve Hu + */ +public class UnifiedSecurityHandler implements MiddlewareHandler { + static final Logger logger = LoggerFactory.getLogger(UnifiedSecurityHandler.class); + static final String BEARER_PREFIX = "BEARER"; + static final String BASIC_PREFIX = "BASIC"; + static final String API_KEY = "apikey"; + static final String JWT = "jwt"; + static final String MISSING_AUTH_TOKEN = "ERR10002"; + static final String INVALID_AUTHORIZATION_HEADER = "ERR12003"; + static final String HANDLER_NOT_FOUND = "ERR11200"; + static final String MISSING_PATH_PREFIX_AUTH = "ERR10078"; + static UnifiedSecurityConfig config; + // make this static variable public so that it can be accessed from the server-info module + private volatile HttpHandler next; + + public UnifiedSecurityHandler() { + logger.info("UnifiedSecurityHandler starts"); + config = UnifiedSecurityConfig.load(); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (logger.isDebugEnabled()) + logger.debug("UnifiedSecurityHandler.handleRequest starts."); + String reqPath = exchange.getRequestPath(); + // check if the path prefix is in the anonymousPrefixes list. If yes, skip all other check and goes to next handler. + if (config.getAnonymousPrefixes() != null && config.getAnonymousPrefixes().stream().anyMatch(reqPath::startsWith)) { + if(logger.isTraceEnabled()) + logger.trace("Skip request path base on anonymousPrefixes for " + reqPath); + Handler.next(exchange, next); + if (logger.isDebugEnabled()) + logger.debug("UnifiedSecurityHandler.handleRequest ends."); + return; + } + if(config.getPathPrefixAuths() != null) { + boolean found = false; + // iterate each entry to check enabled security methods. + for(UnifiedPathPrefixAuth pathPrefixAuth: config.getPathPrefixAuths()) { + if(reqPath.startsWith(pathPrefixAuth.getPathPrefix())) { + found = true; + if(logger.isTraceEnabled()) logger.trace("Found with requestPath = " + reqPath + " prefix = " + pathPrefixAuth.getPathPrefix()); + // check jwt and basic first with authorization header, then check the apikey if it is enabled. + if(pathPrefixAuth.isBasic() || pathPrefixAuth.isJwt()) { + String authorization = exchange.getRequestHeaders().getFirst(Headers.AUTHORIZATION); + if(authorization == null) { + logger.error("Basic or JWT is enabled and authorization header is missing."); + setExchangeStatus(exchange, MISSING_AUTH_TOKEN); + if(logger.isDebugEnabled()) + logger.debug("UnifiedSecurityHandler.handleRequest ends with an error."); + exchange.endExchange(); + } else { + // make sure that the length is greater than 5. + if(authorization.trim().length() <= 5) { + logger.error("Invalid/Unsupported authorization header {}", authorization); + setExchangeStatus(exchange, INVALID_AUTHORIZATION_HEADER, authorization); + exchange.endExchange(); + return; + } + // check if it is basic or bearer and handler it differently. + if(BASIC_PREFIX.equalsIgnoreCase(authorization.substring(0, 5))) { + Map handlers = Handler.getHandlers(); + BasicAuthHandler handler = (BasicAuthHandler) handlers.get(BASIC_PREFIX.toLowerCase()); + if(handler == null) { + logger.error("Cannot find BasicAuthHandler with alias name basic."); + setExchangeStatus(exchange, HANDLER_NOT_FOUND, "com.networknt.basicauth.BasicAuthHandler@basic"); + exchange.endExchange(); + return; + } else { + handler.handleBasicAuth(exchange, reqPath, authorization); + break; + } + } else if (BEARER_PREFIX.equalsIgnoreCase(authorization.substring(0, 6))) { + Map handlers = Handler.getHandlers(); + JwtVerifyHandler handler = (JwtVerifyHandler) handlers.get(JWT); + if(handler == null) { + logger.error("Cannot find JwtVerifyHandler with alias name jwt."); + setExchangeStatus(exchange, HANDLER_NOT_FOUND, "com.networknt.openapi.JwtVerifyHandler@jwt"); + exchange.endExchange(); + return; + } else { + // get the jwkServiceIds list. + if(handler.handleJwt(exchange, pathPrefixAuth.getPathPrefix(), reqPath, pathPrefixAuth.getJwkServiceIds())) { + // verification is passed, go to the next handler in the chain. + break; + } else { + // verification is not passed and an error is returned. + return; + } + } + + } else { + logger.error("Invalid/Unsupported authorization header {}", authorization.substring(0, 10)); + setExchangeStatus(exchange, INVALID_AUTHORIZATION_HEADER, authorization.substring(0, 10)); + exchange.endExchange(); + return; + } + } + } else if (pathPrefixAuth.isApikey()) { + Map handlers = Handler.getHandlers(); + ApiKeyHandler handler = (ApiKeyHandler) handlers.get(API_KEY); + if(handler == null) { + logger.error("Cannot find ApiKeyHandler with alias name apikey."); + setExchangeStatus(exchange, HANDLER_NOT_FOUND, "com.networknt.apikey.ApiKeyHandler@apikey"); + exchange.endExchange(); + return; + } else { + handler.handleApiKey(exchange, reqPath); + break; + } + + } + } + } + if(!found) { + // cannot find the prefix auth entry for request path. + logger.error("Cannot find prefix entry in pathPrefixAuths for " + reqPath); + setExchangeStatus(exchange, MISSING_PATH_PREFIX_AUTH, reqPath); + exchange.endExchange(); + return; + } + } else { + // pathPrefixAuths is not defined in the values.yml + logger.error("Cannot find pathPrefixAuths definition for " + reqPath); + setExchangeStatus(exchange, MISSING_PATH_PREFIX_AUTH, reqPath); + exchange.endExchange(); + return; + } + + if(logger.isDebugEnabled()) logger.debug("UnifiedSecurityHandler.handleRequest ends."); + Handler.next(exchange, next); + } + + @Override + public HttpHandler getNext() { + return next; + } + + @Override + public MiddlewareHandler setNext(HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + @Override + public boolean isEnabled() { + return config.isEnabled(); + } + + @Override + public void register() { + ModuleRegistry.registerModule(UnifiedSecurityHandler.class.getName(), Config.getInstance().getJsonMapConfigNoCache(UnifiedSecurityHandler.CONFIG_NAME), null); + } + + @Override + public void reload() { + config.reload(); + ModuleRegistry.registerModule(UnifiedSecurityHandler.class.getName(), Config.getInstance().getJsonMapConfigNoCache(UnifiedSecurityHandler.CONFIG_NAME), null); + } + +} diff --git a/openapi-security/src/main/resources/config/unified-security.yml b/openapi-security/src/main/resources/config/unified-security.yml new file mode 100644 index 00000000..b513af97 --- /dev/null +++ b/openapi-security/src/main/resources/config/unified-security.yml @@ -0,0 +1,31 @@ +# unified-security.yml +# indicate if this handler is enabled. By default, it will be enabled if it is injected into the +# request/response chain in the handler.yml configuration. +enabled: ${unified-security.enabled:true} +# Anonymous prefixes configuration. A list of request path prefixes. The anonymous prefixes will be checked +# first, and if any path is matched, all other security checks will be bypassed, and the request goes to +# the next handler in the chain. You can use json array or string separated by comma or YAML format. +anonymousPrefixes: ${unified-security.anonymousPrefixes:} +# String format with comma separator +# /v1/pets,/v1/cats,/v1/dogs +# JSON format as a string +# ["/v1/pets", "/v1/dogs", "/v1/cats"] +# YAML format +# - /v1/pets +# - /v1/dogs +# - /v1/cats +pathPrefixAuths: ${unified-security.pathPrefixAuths:} +# format as a string for config server. +# [{"prefix":"/salesforce","basic":true,"jwt":true,"apikey":true,"jwkServiceIds":"com.networknt.petstore-1.0.0, com.networknt.market-1.0.0"},{"prefix":"/blackrock","basic":true,"jwt":true,"jwkServiceIds":["com.networknt.petstore-1.0.0","com.networknt.market-1.0.0"]}] +# format with YAML for readability + # path prefix security configuration. + # - pathPrefix: /salesforce + # indicate if the basic auth is enabled for this path prefix + # basic: true + # indicate if the jwt token verification is enabled for this path prefix + # jwt: true + # indicate if the apikey is enabled for this path prefix + # apikey: true + # if jwt is true and there are two or more jwk servers for the path prefix, then list all the + # serviceIds for jwk in client.yml + # jwkServiceIds: service1,service2 diff --git a/openapi-security/src/test/java/com/networknt/openapi/JwtVerifierHandlerMultipleSpecsTest.java b/openapi-security/src/test/java/com/networknt/openapi/JwtVerifierHandlerMultipleSpecsTest.java index 29d5f03d..c0d097c7 100644 --- a/openapi-security/src/test/java/com/networknt/openapi/JwtVerifierHandlerMultipleSpecsTest.java +++ b/openapi-security/src/test/java/com/networknt/openapi/JwtVerifierHandlerMultipleSpecsTest.java @@ -51,7 +51,7 @@ public static void setUp() { OpenApiHandler openApiHandler = new OpenApiHandler(OpenApiHandlerConfig.load("openapi-handler-multiple")); openApiHandler.setNext(jwtVerifyHandler); server = Undertow.builder() - .addHttpListener(7080, "localhost") + .addHttpListener(7081, "localhost") .setHandler(openApiHandler) .build(); server.start(); @@ -93,7 +93,7 @@ public void testWithCorrectScopeInIdToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -128,7 +128,7 @@ public void testWithCorrectCommaSeperatedScpClaimScopeInIdToken() throws Excepti final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -161,7 +161,7 @@ public void testWithCorrectSpaceSeperatedScpClaimScopeInIdToken() throws Excepti final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -194,7 +194,7 @@ public void testWithCorrectSpaceSeperatedScopeClaimScopeInIdToken() throws Excep final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -224,7 +224,7 @@ public void testUnmatchedScopeInIdToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -256,7 +256,7 @@ public void testWithCorrectScopeInScopeToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -287,7 +287,7 @@ public void testUnmatchedScopeInScopeToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } diff --git a/openapi-security/src/test/java/com/networknt/openapi/JwtVerifyHandlerTest.java b/openapi-security/src/test/java/com/networknt/openapi/JwtVerifyHandlerTest.java index e3e8f95d..090edec6 100644 --- a/openapi-security/src/test/java/com/networknt/openapi/JwtVerifyHandlerTest.java +++ b/openapi-security/src/test/java/com/networknt/openapi/JwtVerifyHandlerTest.java @@ -66,7 +66,7 @@ public static void setUp() { OpenApiHandler openApiHandler = new OpenApiHandler(); openApiHandler.setNext(jwtVerifyHandler); server = Undertow.builder() - .addHttpListener(7080, "localhost") + .addHttpListener(7081, "localhost") .setHandler(openApiHandler) .build(); server.start(); @@ -108,7 +108,7 @@ public void testWithCorrectScopeInIdToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -141,7 +141,7 @@ public void testWithCorrectCommaSeperatedScpClaimScopeInIdToken() throws Excepti final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -174,7 +174,7 @@ public void testWithCorrectSpaceSeperatedScpClaimScopeInIdToken() throws Excepti final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -207,7 +207,7 @@ public void testWithCorrectSpaceSeperatedScopeClaimScopeInIdToken() throws Excep final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -237,7 +237,7 @@ public void testUnmatchedScopeInIdToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -269,7 +269,7 @@ public void testWithCorrectScopeInScopeToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -300,7 +300,7 @@ public void testUnmatchedScopeInScopeToken() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } @@ -333,7 +333,7 @@ public void testH2CDisabledRequest() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final ClientConnection connection; try { - connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + connection = client.connect(new URI("http://localhost:7081"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); } catch (Exception e) { throw new ClientException(e); } diff --git a/openapi-security/src/test/java/com/networknt/openapi/TestServer.java b/openapi-security/src/test/java/com/networknt/openapi/TestServer.java new file mode 100644 index 00000000..35cab921 --- /dev/null +++ b/openapi-security/src/test/java/com/networknt/openapi/TestServer.java @@ -0,0 +1,50 @@ +package com.networknt.openapi; + +import com.networknt.server.Server; +import com.networknt.server.ServerConfig; +import org.junit.rules.ExternalResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +public class TestServer extends ExternalResource { + static final Logger logger = LoggerFactory.getLogger(TestServer.class); + + private static final AtomicInteger refCount = new AtomicInteger(0); + private static Server server; + + private static final TestServer instance = new TestServer(); + + public static TestServer getInstance () { + return instance; + } + + private TestServer() { + + } + + public ServerConfig getServerConfig() { + return Server.getServerConfig(); + } + + @Override + protected void before() { + try { + if (refCount.get() == 0) { + Server.start(); + } + } + finally { + refCount.getAndIncrement(); + } + } + + @Override + protected void after() { + refCount.getAndDecrement(); + if (refCount.get() == 0) { + Server.stop(); + } + } +} diff --git a/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityConfigTest.java b/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityConfigTest.java new file mode 100644 index 00000000..161e4a85 --- /dev/null +++ b/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityConfigTest.java @@ -0,0 +1,70 @@ +package com.networknt.openapi; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class UnifiedSecurityConfigTest { + @Test + public void testLoadConfig() { + UnifiedSecurityConfig config = UnifiedSecurityConfig.load(); + Assert.assertTrue(config.isEnabled()); + // check anonymousPrefixes have /v1/pets + List anonymousPrefixes = config.getAnonymousPrefixes(); + Assert.assertTrue(anonymousPrefixes.contains("/v1/dogs")); + Assert.assertTrue(anonymousPrefixes.contains("/v1/cats")); + Assert.assertEquals(2, config.getAnonymousPrefixes().size()); + // check the pathPrefixAuths + Assert.assertEquals(4, config.getPathPrefixAuths().size()); + UnifiedPathPrefixAuth auth1 = config.getPathPrefixAuths().get(0); + Assert.assertTrue(auth1.isBasic()); + Assert.assertTrue(auth1.isJwt()); + Assert.assertTrue(auth1.isApikey()); + Assert.assertEquals(2, auth1.getJwkServiceIds().size()); + Assert.assertEquals("com.networknt.market-1.0.0", auth1.getJwkServiceIds().get(1)); + UnifiedPathPrefixAuth auth2 = config.getPathPrefixAuths().get(1); + Assert.assertTrue(auth2.isBasic()); + Assert.assertTrue(auth2.isJwt()); + Assert.assertFalse(auth2.isApikey()); + Assert.assertEquals(2, auth2.getJwkServiceIds().size()); + Assert.assertEquals("com.networknt.market-1.0.0", auth2.getJwkServiceIds().get(1)); + } + + @Test + public void testLoadStringConfig() { + UnifiedSecurityConfig config = UnifiedSecurityConfig.load("unified-security-json"); + Assert.assertTrue(config.isEnabled()); + // check anonymousPrefixes have /v1/pets + List anonymousPrefixes = config.getAnonymousPrefixes(); + Assert.assertTrue(anonymousPrefixes.contains("/v1/pets")); + Assert.assertTrue(anonymousPrefixes.contains("/v1/cats")); + Assert.assertEquals(2, config.getAnonymousPrefixes().size()); + // check the pathPrefixAuths + Assert.assertEquals(2, config.getPathPrefixAuths().size()); + UnifiedPathPrefixAuth auth1 = config.getPathPrefixAuths().get(0); + Assert.assertTrue(auth1.isBasic()); + Assert.assertTrue(auth1.isJwt()); + Assert.assertTrue(auth1.isApikey()); + Assert.assertEquals(2, auth1.getJwkServiceIds().size()); + Assert.assertEquals("com.networknt.market-1.0.0", auth1.getJwkServiceIds().get(1)); + UnifiedPathPrefixAuth auth2 = config.getPathPrefixAuths().get(1); + Assert.assertTrue(auth2.isBasic()); + Assert.assertTrue(auth2.isJwt()); + Assert.assertFalse(auth2.isApikey()); + Assert.assertEquals(2, auth2.getJwkServiceIds().size()); + Assert.assertEquals("com.networknt.market-1.0.0", auth2.getJwkServiceIds().get(1)); + } + + @Test + public void testLoadNoListConfig() { + UnifiedSecurityConfig config = UnifiedSecurityConfig.load("unified-security-nolist"); + Assert.assertTrue(config.isEnabled()); + // check anonymousPrefixes have /v1/pets + List anonymousPrefixes = config.getAnonymousPrefixes(); + Assert.assertNull(anonymousPrefixes); + // check the pathPrefixAuths + Assert.assertNull(config.getPathPrefixAuths()); + } + +} diff --git a/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityHandlerTest.java b/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityHandlerTest.java new file mode 100644 index 00000000..3e2a4273 --- /dev/null +++ b/openapi-security/src/test/java/com/networknt/openapi/UnifiedSecurityHandlerTest.java @@ -0,0 +1,526 @@ +package com.networknt.openapi; + +import com.networknt.client.Http2Client; +import com.networknt.config.Config; +import com.networknt.exception.ClientException; +import com.networknt.httpstring.HttpStringConstants; +import com.networknt.status.Status; +import io.undertow.Handlers; +import io.undertow.Undertow; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.server.HttpHandler; +import io.undertow.server.RoutingHandler; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class UnifiedSecurityHandlerTest { + static final Logger logger = LoggerFactory.getLogger(UnifiedSecurityHandlerTest.class); + + @ClassRule + public static TestServer server = TestServer.getInstance(); + + static final boolean enableHttp2 = server.getServerConfig().isEnableHttp2(); + static final boolean enableHttps = server.getServerConfig().isEnableHttps(); + static final int httpPort = server.getServerConfig().getHttpPort(); + static final int httpsPort = server.getServerConfig().getHttpsPort(); + static final String url = enableHttp2 || enableHttps ? "https://localhost:" + httpsPort : "http://localhost:" + httpPort; + + static RoutingHandler getTestHandler() { + return Handlers.routing() + .add(Methods.GET, "/v1/pets/{petId}", exchange -> { + Map examples = new HashMap<>(); + examples.put("application/xml", StringEscapeUtils.unescapeHtml4("<Pet> <id>123456</id> <name>doggie</name> <photoUrls> <photoUrls>string</photoUrls> </photoUrls> <tags> </tags> <status>string</status></Pet>")); + examples.put("application/json", StringEscapeUtils.unescapeHtml4("{ "photoUrls" : [ "aeiou" ], "name" : "doggie", "id" : 123456789, "category" : { "name" : "aeiou", "id" : 123456789 }, "tags" : [ { "name" : "aeiou", "id" : 123456789 } ], "status" : "aeiou"}")); + if (examples.size() > 0) { + exchange.getResponseHeaders().add(new HttpString("Content-Type"), "application/json"); + exchange.getResponseSender().send((String) examples.get("application/json")); + } else { + exchange.endExchange(); + } + }) + .add(Methods.GET, "/v1/pets", exchange -> exchange.getResponseSender().send("get")); + } + private static String encodeCredentialsFullFormat(String username, String password, String separator) { + String cred; + if(password != null) { + cred = username + separator + password; + } else { + cred = username; + } + String encodedValue; + byte[] encodedBytes = Base64.encodeBase64(cred.getBytes(UTF_8)); + encodedValue = new String(encodedBytes, UTF_8); + return encodedValue; + } + + private static String encodeCredentials(String username, String password) { + return encodeCredentialsFullFormat(username, password, ":"); + } + + /** + * Send a request without authorization header but put the request path prefix into the anonymousPrefixes list. We are expecting + * 200 status code as all the security methods bypassed. + * + * @throws Exception + */ + @Test + public void testAnonymousPrefix() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/dogs/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + /** + * Test basic header to ensure that BasicAuthHandler is invoked and the validation is done correctly. For this request, + * we have passed the right credentials to the basic header and we are expecting 200 response. + */ + @Test + public void testWithBasicHeader() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/salesforce").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "BASIC " + encodeCredentials("user1", "user1pass")); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + /** + * Test basic authentication with a wrong password and expecting an error message with 401 status code. + * @throws Exception + */ + @Test + public void testWithBasicHeaderWithWrongPassword() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/salesforce").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "BASIC " + encodeCredentials("user1", "wrong")); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + String responseBody = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + logger.debug("statusCode = " + statusCode); + logger.debug("responseBody = " + responseBody); + Assert.assertEquals(401, statusCode); + if (statusCode == 401) { + Assert.assertTrue(responseBody.contains("INVALID_USERNAME_OR_PASSWORD")); + } + } + + /** + * Test basic authentication with a basic prefix and a space for the authorization header. Expecting an error message with + * status code 401. + * @throws Exception + */ + @Test + public void testBasicWithSpace() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/salesforce").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "BASIC "); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + String responseBody = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + logger.debug("statusCode = " + statusCode); + logger.debug("responseBody = " + responseBody); + Assert.assertEquals(401, statusCode); + if (statusCode == 401) { + Assert.assertTrue(responseBody.contains("INVALID_AUTHORIZATION_HEADER")); + } + } + + /** + * Test apikey header to ensure that ApiKeyHandler is invoked and expect the right response. + */ + @Test + public void testWithApiKeyHeader() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/test1").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(new HttpString("x-gateway-apikey"), "abcdefg"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + /** + * Test apikey header to ensure that ApiKeyHandler is invoked and expect an error response as the apikey is wrong + */ + @Test + public void testWithApiKeyHeaderWongKey() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/test1").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(new HttpString("x-gateway-apikey"), "wrong"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + String responseBody = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + logger.debug("statusCode = " + statusCode); + logger.debug("responseBody = " + responseBody); + Assert.assertEquals(401, statusCode); + if (statusCode == 401) { + Assert.assertTrue(responseBody.contains("API_KEY_MISMATCH")); + } + } + + /** + * Test a path that is not configured in the pathPrefixAuths. Expect an error response. + * @throws Exception + */ + @Test + public void testWrongPathNotConfigured() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/wrong").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + String responseBody = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + logger.debug("statusCode = " + statusCode); + logger.debug("responseBody = " + responseBody); + Assert.assertEquals(400, statusCode); + if (statusCode == 400) { + Assert.assertTrue(responseBody.contains("MISSING_PATH_PREFIX_AUTH")); + } + } + + + /** + * Test space separated scopes with the key 'scp' + */ + @Test + public void testWithCorrectSpaceSeparatedScpClaimScopeInIdToken() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTkxOTc4NjIxNiwianRpIjoiNjRoVWRiNWJ0OFYzRkJzOFZDNXZRQSIsImlhdCI6MTYwNDQyNjIxNiwibmJmIjoxNjA0NDI2MDk2LCJ2ZXJzaW9uIjoiMS4wIiwiY2xpZW50X2lkIjoiZjdkNDIzNDgtYzY0Ny00ZWZiLWE1MmQtNGM1Nzg3NDIxZTczIiwic2NwIjoid3JpdGU6cGV0cyByZWFkOnBldHMifQ.MPRUBQRbN-13poJ1XV0jHuJgGbOuOglojDzQScEo7WU2UwHseLl_HaZqPEHm-eW8AAmLZ_tzKlchAJR7OVP3CPWgsQNrb2uR3uf4dgSBdD2ZmMPdT1m6KAFhNVzwsEx3vdweL6OlZMm3x03nz3eIRKW8gdGoeTq08HGOzTjKpsFYVMSgdv6nf0HfyOeg5dhByVsdqnhKdig3bMyaHo4HlKQfN-eSaMusG9QPDbQoP0IBWRrFlv63iNrEm5EX9zx6K81awWR7K5Iu_WIJkGZU_Fm0qHee9Ur4_1OdXLOLRKIvNE150jS7vX_a0YGHteLgkvAjs_AtVaUzVnAnHE46lw"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + + /** + * Test space separated scopes with the key 'scope' + */ + @Test + public void testWithCorrectSpaceSeparatedScopeClaimScopeInIdToken() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTkxOTc4NjIxNiwianRpIjoid1RtZFhWRE83VHVaaWw1TG5YYks1USIsImlhdCI6MTYwNDQyNjIxNiwibmJmIjoxNjA0NDI2MDk2LCJ2ZXJzaW9uIjoiMS4wIiwiY2xpZW50X2lkIjoiZjdkNDIzNDgtYzY0Ny00ZWZiLWE1MmQtNGM1Nzg3NDIxZTczIiwic2NvcGUiOiJ3cml0ZTpwZXRzIHJlYWQ6cGV0cyJ9.P4WSx19ueJDKDZBLvy_esrvQpGaeKwHpCnXtf7o89XXKkpRlAyFlJj4bkclHi8H-gi1g8xqnna2ygKVQUbcjzPDt2ks8ZpZTqRAeYQP6dWJZXEww_VV_DSJZTLNq_zjN9JGllvO5A3C3SdV536V0P7w249mSL4JXFaAwdMgnmnneTdP54qyaGH9w0QYjffdx8ODG8JMq-YY434jQ8q81hXKxu5OF1kOpGSqA7bJ3_kAtx5aYPtoxOv4xwv_-ear2meKbMTo0yKVNIhXI6GlfUEiJ1tgZ0Ni89XBxTMaEy7I0t3rvB0ko9ONTyOtnH3cLdwQeqnTP6-TMps1WUuxYxQ"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + @Test + public void testUnmatchedScopeInIdToken() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiTVJiZHdlQ295eG13a2ZUM3lVWGloQSIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6ImVyaWMiLCJ1c2VyX3R5cGUiOiJFTVBMT1lFRSIsImNsaWVudF9pZCI6ImY3ZDQyMzQ4LWM2NDctNGVmYi1hNTJkLTRjNTc4NzQyMWU3MiIsInNjb3BlIjpbIkFUTVAxMDAwLnciLCJBVE1QMTAwMC5yIl19.VOEggO6UIMHNJLrxShGivCh7sGyHiz7h9FqDjlKwywGP9xKbVTTODy2-FitUaS1Y2vjiHlJ0TNyxmj1SO11YwYnJlW1zn-6vfKWKI70DyvRwsvSX_8Z2fj0jPUiBqezwKRtLCHSsmiEpMrW6YQHYw0qzZ9kkMhiH2uFpZNCekOQWL1piRn1xVQkUmeFiTDvJQESHadFzw-9x0klO7-SxgKeHHDroxnpbLv2j795oMTB1gM_wJP6HO_M-gK6N1Uh6zssfnbyFReRNWkhZFOp3Y8DvwpfKhqXIVGUc_5WsO9M-y66icClVNl5zwLSmjsrNtqZkmeBCwQ6skBnRLfMocQ"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(403, statusCode); + if (statusCode == 403) { + Status status = Config.getInstance().getMapper().readValue(reference.get().getAttachment(Http2Client.RESPONSE_BODY), Status.class); + Assert.assertNotNull(status); + Assert.assertEquals("ERR10005", status.getCode()); + } + } + + @Test + public void testWithCorrectScopeInScopeToken() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiV0Z1VVZneE83dmxKUm5XUlllMjE1dyIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6InN0ZXZlIiwidXNlcl90eXBlIjoiRU1QTE9ZRUUiLCJjbGllbnRfaWQiOiJmN2Q0MjM0OC1jNjQ3LTRlZmItYTUyZC00YzU3ODc0MjFlNzIiLCJzY29wZSI6WyJ3cml0ZTpwZXRzIiwicmVhZDpwZXRzIl19.ZDlD_JbtHMqfx8EWOlOXI0zFGjB_pJ6yXWpxoE03o2yQnCUq1zypaDTJWSiy-BPIiQAxwDV09L3SN7RsOcgJ3y2LLFhgqIXhcHoePxoz52LPOeeiihG2kcrgBm-_VMq0uUykLrD-ljSmmSm1Hai_dx0WiYGAEJf-TiD1mgzIUTlhogYrjFKlp2NaYHxr7yjzEGefKv4DWdjtlEMmX_cXkqPgxra_omzyxeWE-n0b7f_r7Hr5HkxnmZ23gkZcvFXfVWKEp2t0_dYmNCbSVDavAjNanvmWsNThYNglFRvF0lm8kl7jkfMO1pTa0WLcBLvOO2y_jRWjieFCrc0ksbIrXA"); + request.getRequestHeaders().put(HttpStringConstants.SCOPE_TOKEN, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiV0Z1VVZneE83dmxKUm5XUlllMjE1dyIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6InN0ZXZlIiwidXNlcl90eXBlIjoiRU1QTE9ZRUUiLCJjbGllbnRfaWQiOiJmN2Q0MjM0OC1jNjQ3LTRlZmItYTUyZC00YzU3ODc0MjFlNzIiLCJzY29wZSI6WyJ3cml0ZTpwZXRzIiwicmVhZDpwZXRzIl19.ZDlD_JbtHMqfx8EWOlOXI0zFGjB_pJ6yXWpxoE03o2yQnCUq1zypaDTJWSiy-BPIiQAxwDV09L3SN7RsOcgJ3y2LLFhgqIXhcHoePxoz52LPOeeiihG2kcrgBm-_VMq0uUykLrD-ljSmmSm1Hai_dx0WiYGAEJf-TiD1mgzIUTlhogYrjFKlp2NaYHxr7yjzEGefKv4DWdjtlEMmX_cXkqPgxra_omzyxeWE-n0b7f_r7Hr5HkxnmZ23gkZcvFXfVWKEp2t0_dYmNCbSVDavAjNanvmWsNThYNglFRvF0lm8kl7jkfMO1pTa0WLcBLvOO2y_jRWjieFCrc0ksbIrXA"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(200, statusCode); + if (statusCode == 200) { + Assert.assertNotNull(reference.get().getAttachment(Http2Client.RESPONSE_BODY)); + } + } + + @Test + public void testUnmatchedScopeInScopeToken() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiTVJiZHdlQ295eG13a2ZUM3lVWGloQSIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6ImVyaWMiLCJ1c2VyX3R5cGUiOiJFTVBMT1lFRSIsImNsaWVudF9pZCI6ImY3ZDQyMzQ4LWM2NDctNGVmYi1hNTJkLTRjNTc4NzQyMWU3MiIsInNjb3BlIjpbIkFUTVAxMDAwLnciLCJBVE1QMTAwMC5yIl19.VOEggO6UIMHNJLrxShGivCh7sGyHiz7h9FqDjlKwywGP9xKbVTTODy2-FitUaS1Y2vjiHlJ0TNyxmj1SO11YwYnJlW1zn-6vfKWKI70DyvRwsvSX_8Z2fj0jPUiBqezwKRtLCHSsmiEpMrW6YQHYw0qzZ9kkMhiH2uFpZNCekOQWL1piRn1xVQkUmeFiTDvJQESHadFzw-9x0klO7-SxgKeHHDroxnpbLv2j795oMTB1gM_wJP6HO_M-gK6N1Uh6zssfnbyFReRNWkhZFOp3Y8DvwpfKhqXIVGUc_5WsO9M-y66icClVNl5zwLSmjsrNtqZkmeBCwQ6skBnRLfMocQ"); + request.getRequestHeaders().put(HttpStringConstants.SCOPE_TOKEN, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiTVJiZHdlQ295eG13a2ZUM3lVWGloQSIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6ImVyaWMiLCJ1c2VyX3R5cGUiOiJFTVBMT1lFRSIsImNsaWVudF9pZCI6ImY3ZDQyMzQ4LWM2NDctNGVmYi1hNTJkLTRjNTc4NzQyMWU3MiIsInNjb3BlIjpbIkFUTVAxMDAwLnciLCJBVE1QMTAwMC5yIl19.VOEggO6UIMHNJLrxShGivCh7sGyHiz7h9FqDjlKwywGP9xKbVTTODy2-FitUaS1Y2vjiHlJ0TNyxmj1SO11YwYnJlW1zn-6vfKWKI70DyvRwsvSX_8Z2fj0jPUiBqezwKRtLCHSsmiEpMrW6YQHYw0qzZ9kkMhiH2uFpZNCekOQWL1piRn1xVQkUmeFiTDvJQESHadFzw-9x0klO7-SxgKeHHDroxnpbLv2j795oMTB1gM_wJP6HO_M-gK6N1Uh6zssfnbyFReRNWkhZFOp3Y8DvwpfKhqXIVGUc_5WsO9M-y66icClVNl5zwLSmjsrNtqZkmeBCwQ6skBnRLfMocQ"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(403, statusCode); + if (statusCode == 403) { + Status status = Config.getInstance().getMapper().readValue(reference.get().getAttachment(Http2Client.RESPONSE_BODY), Status.class); + Assert.assertNotNull(status); + Assert.assertEquals("ERR10006", status.getCode()); + } + } + + @Test + public void testH2CDisabledRequest() throws Exception { + final Http2Client client = Http2Client.getInstance(); + final CountDownLatch latch = new CountDownLatch(1); + final ClientConnection connection; + try { + connection = client.connect(new URI("http://localhost:7080"), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.EMPTY).get(); + } catch (Exception e) { + throw new ClientException(e); + } + final AtomicReference reference = new AtomicReference<>(); + try { + ClientRequest request = new ClientRequest().setPath("/v1/pets/111").setMethod(Methods.GET); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + request.getRequestHeaders().put(Headers.AUTHORIZATION, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiTVJiZHdlQ295eG13a2ZUM3lVWGloQSIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6ImVyaWMiLCJ1c2VyX3R5cGUiOiJFTVBMT1lFRSIsImNsaWVudF9pZCI6ImY3ZDQyMzQ4LWM2NDctNGVmYi1hNTJkLTRjNTc4NzQyMWU3MiIsInNjb3BlIjpbIkFUTVAxMDAwLnciLCJBVE1QMTAwMC5yIl19.VOEggO6UIMHNJLrxShGivCh7sGyHiz7h9FqDjlKwywGP9xKbVTTODy2-FitUaS1Y2vjiHlJ0TNyxmj1SO11YwYnJlW1zn-6vfKWKI70DyvRwsvSX_8Z2fj0jPUiBqezwKRtLCHSsmiEpMrW6YQHYw0qzZ9kkMhiH2uFpZNCekOQWL1piRn1xVQkUmeFiTDvJQESHadFzw-9x0klO7-SxgKeHHDroxnpbLv2j795oMTB1gM_wJP6HO_M-gK6N1Uh6zssfnbyFReRNWkhZFOp3Y8DvwpfKhqXIVGUc_5WsO9M-y66icClVNl5zwLSmjsrNtqZkmeBCwQ6skBnRLfMocQ"); + request.getRequestHeaders().put(HttpStringConstants.SCOPE_TOKEN, "Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTgwNTEzNjU1MSwianRpIjoiTVJiZHdlQ295eG13a2ZUM3lVWGloQSIsImlhdCI6MTQ4OTc3NjU1MSwibmJmIjoxNDg5Nzc2NDMxLCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6ImVyaWMiLCJ1c2VyX3R5cGUiOiJFTVBMT1lFRSIsImNsaWVudF9pZCI6ImY3ZDQyMzQ4LWM2NDctNGVmYi1hNTJkLTRjNTc4NzQyMWU3MiIsInNjb3BlIjpbIkFUTVAxMDAwLnciLCJBVE1QMTAwMC5yIl19.VOEggO6UIMHNJLrxShGivCh7sGyHiz7h9FqDjlKwywGP9xKbVTTODy2-FitUaS1Y2vjiHlJ0TNyxmj1SO11YwYnJlW1zn-6vfKWKI70DyvRwsvSX_8Z2fj0jPUiBqezwKRtLCHSsmiEpMrW6YQHYw0qzZ9kkMhiH2uFpZNCekOQWL1piRn1xVQkUmeFiTDvJQESHadFzw-9x0klO7-SxgKeHHDroxnpbLv2j795oMTB1gM_wJP6HO_M-gK6N1Uh6zssfnbyFReRNWkhZFOp3Y8DvwpfKhqXIVGUc_5WsO9M-y66icClVNl5zwLSmjsrNtqZkmeBCwQ6skBnRLfMocQ"); + request.getRequestHeaders().put(Headers.CONNECTION, "upgrade"); + request.getRequestHeaders().put(Headers.UPGRADE, "foo/2"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + } catch (Exception e) { + logger.error("Exception: ", e); + throw new ClientException(e); + } finally { + IoUtils.safeClose(connection); + } + int statusCode = reference.get().getResponseCode(); + Assert.assertEquals(405, statusCode); + if (statusCode == 405) { + Status status = Config.getInstance().getMapper().readValue(reference.get().getAttachment(Http2Client.RESPONSE_BODY), Status.class); + Assert.assertNotNull(status); + Assert.assertEquals("ERR10008", status.getCode()); + } + } + +} diff --git a/openapi-security/src/test/resources/config/handler.yml b/openapi-security/src/test/resources/config/handler.yml index 2f55042f..c1691d5d 100644 --- a/openapi-security/src/test/resources/config/handler.yml +++ b/openapi-security/src/test/resources/config/handler.yml @@ -1,37 +1,68 @@ +# Handler middleware chain configuration --- enabled: true # Configuration for the LightHttpHandler. The handler is the base class for all middleware, server and health handlers # set the Status Object in the AUDIT_INFO, for auditing purposes # default, if not set:false -auditOnError: true +auditOnError: ${handler.auditOnError:false} # set the StackTrace in the AUDIT_INFO, for auditing purposes # default, if not set:false -auditStackTrace: true +auditStackTrace: ${handler.auditStackTrace:false} -handlers: - - com.networknt.handler.sample.SampleHttpHandler1 - - com.networknt.handler.sample.SampleHttpHandler2 - - com.networknt.handler.sample.SampleHttpHandler3@third +# Base Path of the API endpoints +basePath: ${handler.basePath:/} + +#------------------------------------------------------------------------------ +# Support individual handler chains for each separate endpoint. It allows framework +# handlers like health check, server info to bypass majority of the middleware handlers +# and allows mixing multiple frameworks like OpenAPI and GraphQL in the same instance. +# +# handlers -- list of handlers to be used across chains in this microservice +# including the routing handlers for ALL endpoints +# -- format: fully qualified handler class name@optional:given name +# chains -- allows forming of [1..N] chains, which could be wholly or +# used to form handler chains for each endpoint +# ex.: default chain below, reused partially across multiple endpoints +# paths -- list all the paths to be used for routing within the microservice +# ---- path: the URI for the endpoint (ex.: path: '/v1/pets') +# ---- method: the operation in use (ex.: 'post') +# ---- exec: handlers to be executed -- this element forms the list and +# the order of execution for the handlers +# +# IMPORTANT NOTES: +# - to avoid executing a handler, it has to be removed/commented out in the chain +# or change the enabled:boolean to false for a middleware handler configuration. +# - all handlers, routing handler included, are to be listed in the execution chain +# - for consistency, give a name to each handler; it is easier to refer to a name +# vs a fully qualified class name and is more elegant +# - you can list in chains the fully qualified handler class names, and avoid using the +# handlers element altogether +#------------------------------------------------------------------------------ +handlers: ${handler.handlers:} chains: - secondBeforeFirst: - - com.networknt.handler.sample.SampleHttpHandler2 - - com.networknt.handler.sample.SampleHttpHandler1 + default: ${handler.chains.default:} paths: - - path: '/test' - method: 'get' + - path: '/*' + method: 'GET' + exec: + - default + - path: '/*' + method: 'POST' + exec: + - default + - path: '/*' + method: 'PUT' + exec: + - default + - path: '/*' + method: 'DELETE' exec: - - secondBeforeFirst - - third - - path: '/v2/health' - method: 'post' + - default + - path: '/*' + method: 'PATCH' exec: - - secondBeforeFirst - - third -# If there is no matched path, then it goes here first. If this is not set, then an error -# will be returned. -defaultHandlers: - - third + - default diff --git a/openapi-security/src/test/resources/config/openapi.yaml b/openapi-security/src/test/resources/config/openapi.yaml index ae2c7d47..cdc13c10 100644 --- a/openapi-security/src/test/resources/config/openapi.yaml +++ b/openapi-security/src/test/resources/config/openapi.yaml @@ -7,6 +7,18 @@ info: servers: - url: http://petstore.swagger.io/v1 paths: + /wrong: + get: + summary: List of wrong + operationId: listWrong + /test1: + get: + summary: List of test1 + operationId: listTest1 + /salesforce: + get: + summary: List salesforce + operationId: listSalesforce /pets: get: summary: List all pets @@ -76,6 +88,18 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /dogs/{dogId}: + get: + summary: Info for a specific pet + operationId: showPetById + parameters: + - name: dogId + in: path + required: true + description: The id of the dog to retrieve + schema: + type: string + /pets/{petId}: get: summary: Info for a specific pet diff --git a/openapi-security/src/test/resources/config/unified-security-json.yml b/openapi-security/src/test/resources/config/unified-security-json.yml new file mode 100644 index 00000000..0528dac8 --- /dev/null +++ b/openapi-security/src/test/resources/config/unified-security-json.yml @@ -0,0 +1,9 @@ +# unified-security.yml +# indicate if this handler is enabled. By default, it will be enabled if it is injected into the +# request/response chain in the handler.yml configuration. +enabled: ${unified-security.enabled:true} +# Anonymous prefixes configuration. A list of request path prefixes. The anonymous prefixes will be checked +# first, and if any path is matched, all other security checks will be bypassed, and the request goes to +# the next handler in the chain. You can use json array or string separated by comma or YAML format. +anonymousPrefixes: /v1/pets,/v1/cats +pathPrefixAuths: [{"prefix":"/salesforce","basic":true,"jwt":true,"apikey":true,"jwkServiceIds":"com.networknt.petstore-1.0.0, com.networknt.market-1.0.0"},{"prefix":"/blackrock","basic":true,"jwt":true,"jwkServiceIds":["com.networknt.petstore-1.0.0","com.networknt.market-1.0.0"]}] diff --git a/openapi-security/src/test/resources/config/unified-security-nolist.yml b/openapi-security/src/test/resources/config/unified-security-nolist.yml new file mode 100644 index 00000000..0f7314f8 --- /dev/null +++ b/openapi-security/src/test/resources/config/unified-security-nolist.yml @@ -0,0 +1,9 @@ +# unified-security.yml +# indicate if this handler is enabled. By default, it will be enabled if it is injected into the +# request/response chain in the handler.yml configuration. +enabled: ${unified-security.enabled:true} +# Anonymous prefixes configuration. A list of request path prefixes. The anonymous prefixes will be checked +# first, and if any path is matched, all other security checks will be bypassed, and the request goes to +# the next handler in the chain. You can use json array or string separated by comma or YAML format. +anonymousPrefixes: +pathPrefixAuths: diff --git a/openapi-security/src/test/resources/config/unified-security.yml b/openapi-security/src/test/resources/config/unified-security.yml new file mode 100644 index 00000000..6be44c04 --- /dev/null +++ b/openapi-security/src/test/resources/config/unified-security.yml @@ -0,0 +1,25 @@ +# unified-security.yml +# indicate if this handler is enabled. By default, it will be enabled if it is injected into the +# request/response chain in the handler.yml configuration. +enabled: ${unified-security.enabled:true} +# Anonymous prefixes configuration. A list of request path prefixes. The anonymous prefixes will be checked +# first, and if any path is matched, all other security checks will be bypassed, and the request goes to +# the next handler in the chain. You can use json array or string separated by comma or YAML format. +anonymousPrefixes: + - /v1/dogs + - /v1/cats + +pathPrefixAuths: + - prefix: /v1/salesforce + basic: true + jwt: true + apikey: true + jwkServiceIds: com.networknt.petstore-1.0.0, com.networknt.market-1.0.0 + - prefix: /v1/blackrock + basic: true + jwt: true + jwkServiceIds: ["com.networknt.petstore-1.0.0", "com.networknt.market-1.0.0"] + - prefix: /v1/test1 + apikey: true + - prefix: /v1/pets + jwt: true \ No newline at end of file diff --git a/openapi-security/src/test/resources/config/values.yml b/openapi-security/src/test/resources/config/values.yml new file mode 100644 index 00000000..2bdf4f41 --- /dev/null +++ b/openapi-security/src/test/resources/config/values.yml @@ -0,0 +1,42 @@ +# server.yml +server.enableHttps: false +server.enableHttp2: false +server.enableHttp: true +server.httpPort: 7080 + + +# handler.yml +handler.basePath: / +handler.handlers: + - com.networknt.openapi.OpenApiHandler@specification + - com.networknt.openapi.JwtVerifyHandler@jwt + - com.networknt.basicauth.BasicAuthHandler@basic + - com.networknt.openapi.UnifiedSecurityHandler@unified + - com.networknt.apikey.ApiKeyHandler@apikey + +handler.chains.default: + - specification + - unified + +# basic-auth.yml +basic.users: + - username: user1 + password: user1pass + paths: + - /v1/address + - /v1/salesforce + - username: user2 + password: CRYPT:08eXg9TmK604+w06RaBlsPQbplU1F1Ez5pkBO/hNr8w= + paths: + - /v2/pet + - /v2/address + - /v2/party + +# apikey.yml +apikey.pathPrefixAuths: + - pathPrefix: /v1/test1 + headerName: x-gateway-apikey + apiKey: abcdefg + - pathPrefix: /v1/test2 + headerName: x-apikey + apiKey: CRYPT:08eXg9TmK604+w06RaBlsPQbplU1F1Ez5pkBO/hNr8w= diff --git a/pom.xml b/pom.xml index aa2fac08..350eb00d 100644 --- a/pom.xml +++ b/pom.xml @@ -166,6 +166,16 @@ security ${version.light-4j} + + com.networknt + basic-auth + ${version.light-4j} + + + com.networknt + api-key + ${version.light-4j} + com.networknt client