You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by mo...@apache.org on 2024/03/13 17:55:08 UTC

(knox) branch master updated: KNOX-3014 - Fix a bug where unauthenticated path configured in shiro provider throw exception (#879)

This is an automated email from the ASF dual-hosted git repository.

more pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 84999b8e1 KNOX-3014 - Fix a bug where unauthenticated path configured in shiro provider throw exception (#879)
84999b8e1 is described below

commit 84999b8e1851c381480b4827979166675f7971d4
Author: Sandeep Moré <mo...@gmail.com>
AuthorDate: Wed Mar 13 13:55:03 2024 -0400

    KNOX-3014 - Fix a bug where unauthenticated path configured in shiro provider throw exception (#879)
    
    * KNOX-3014 - Fix a bug where unauthenticated path configured in shiro provider throw exception
    
    * Formatting changes
    
    * Adding /knoxtoken/api/v1/jwks.json and v1 will be depreciated
    
    * Check at Knox level if the request is configured to be anonymous usign shiro configs. Add unit tests for better test coverage.
---
 gateway-provider-security-shiro/pom.xml            |   9 +
 .../org/apache/knox/gateway/ShiroMessages.java     |  31 +++
 .../deploy/impl/ShiroDeploymentContributor.java    |  14 ++
 .../filter/ShiroSubjectIdentityAdapter.java        | 215 +++++++++++++++------
 gateway-release/home/conf/topologies/sandbox.xml   |   8 +
 .../apache/knox/gateway/GatewayShiroAuthTest.java  |  74 +++++++
 6 files changed, 292 insertions(+), 59 deletions(-)

diff --git a/gateway-provider-security-shiro/pom.xml b/gateway-provider-security-shiro/pom.xml
index 004a62370..fb380c2e6 100644
--- a/gateway-provider-security-shiro/pom.xml
+++ b/gateway-provider-security-shiro/pom.xml
@@ -45,6 +45,10 @@
             <groupId>org.apache.knox</groupId>
             <artifactId>gateway-util-common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-util-urltemplate</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>org.jboss.shrinkwrap</groupId>
@@ -87,6 +91,11 @@
             <artifactId>commons-io</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java
new file mode 100644
index 000000000..5a51664bf
--- /dev/null
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/ShiroMessages.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.knox.gateway;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+
+@Messages(logger="org.apache.knox.gateway")
+public interface ShiroMessages {
+  @Message(level = MessageLevel.INFO, text = "Request {0} matches unauthenticated path configured in topology, letting it through" )
+  void unauthenticatedPathBypass(String uri);
+
+  @Message( level = MessageLevel.WARN, text = "Invalid URL pattern for rule: {0}" )
+  void invalidURLPattern(String rule);
+}
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
index 345727f0d..61b1a4f14 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/deploy/impl/ShiroDeploymentContributor.java
@@ -32,6 +32,7 @@ import org.jboss.shrinkwrap.descriptor.api.webcommon30.SessionConfigType;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 public class ShiroDeploymentContributor extends ProviderDeploymentContributorBase {
 
@@ -43,8 +44,10 @@ public class ShiroDeploymentContributor extends ProviderDeploymentContributorBas
   private static final String SESSION_TIMEOUT = "sessionTimeout";
   private static final String REMEMBER_ME = "rememberme";
   private static final String SHRIO_CONFIG_FILE_NAME = "shiro.ini";
+  private static final String SHIRO_URL_CONFIG = "urls";
   private static final int DEFAULT_SESSION_TIMEOUT = 30; // 30min
 
+
   @Override
   public String getRole() {
     return "authentication";
@@ -132,6 +135,17 @@ public class ShiroDeploymentContributor extends ProviderDeploymentContributorBas
 
     resource.addFilter().name( getName() ).role(
         getRole() ).impl( SHIRO_FILTER_CLASSNAME ).params( params );
+
+    /* Collect all shiro `urls.` params and them as filter params to POST_FILTER_CLASSNAME  */
+    Map<String, String> shiroURLs = providerParams.entrySet().stream().filter(e -> e.getKey().startsWith(SHIRO_URL_CONFIG)).collect(
+        Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+   for(final Map.Entry<String, String> m: shiroURLs.entrySet()) {
+     params.add( resource.createFilterParam()
+         .name( m.getKey() )
+         .value( m.getValue() ) );
+   }
+
     resource.addFilter().name( "Post" + getName() ).role(
         getRole() ).impl( POST_FILTER_CLASSNAME ).params( params );
   }
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
index 4fb945791..0f8ef22b0 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/ShiroSubjectIdentityAdapter.java
@@ -17,21 +17,8 @@
  */
 package org.apache.knox.gateway.filter;
 
-import java.io.IOException;
-import java.security.Principal;
-import java.security.PrivilegedExceptionAction;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.Callable;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.ShiroMessages;
 import org.apache.knox.gateway.audit.api.Action;
 import org.apache.knox.gateway.audit.api.ActionOutcome;
 import org.apache.knox.gateway.audit.api.AuditContext;
@@ -40,20 +27,76 @@ import org.apache.knox.gateway.audit.api.AuditServiceFactory;
 import org.apache.knox.gateway.audit.api.Auditor;
 import org.apache.knox.gateway.audit.api.ResourceType;
 import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.security.GroupPrincipal;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.util.urltemplate.Matcher;
+import org.apache.knox.gateway.util.urltemplate.Parser;
+import org.apache.knox.gateway.util.urltemplate.Template;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.subject.Subject;
 
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
 public class ShiroSubjectIdentityAdapter implements Filter {
+  private static final ShiroMessages LOG = MessagesFactory.get(ShiroMessages.class);
   private static final String SUBJECT_USER_GROUPS = "subject.userGroups";
   private static AuditService auditService = AuditServiceFactory.getAuditService();
   private static Auditor auditor = auditService.getAuditor(
       AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
       AuditConstants.KNOX_COMPONENT_NAME );
+  private static final String SHIRO_URL_CONFIG = "urls";
+  /* Map of shiro url configs */
+  private static Map<String,String> urls = new HashMap<>();
+
+  /* List of URLs with anon authentication */
+  private static List<Matcher> anonUrls = new ArrayList<>();
 
   @Override
   public void init( FilterConfig filterConfig ) throws ServletException {
+    /* Create a shiro urls config map  */
+    final Enumeration<String> params = filterConfig.getInitParameterNames();
+    while (params.hasMoreElements()) {
+      String param = params.nextElement();
+      if (StringUtils.startsWithIgnoreCase(param, SHIRO_URL_CONFIG)) {
+        String value = filterConfig.getInitParameter(param);
+        final String pathParam = param.substring(param.indexOf('.') + 1);
+        urls.put(pathParam, value);
+        if("anon".equalsIgnoreCase(value)) {
+          final Template urlPatternTemplate;
+          final Matcher urlMatcher = new Matcher();
+          try {
+            urlPatternTemplate = Parser.parseTemplate(pathParam);
+            urlMatcher.add(urlPatternTemplate, pathParam);
+            anonUrls.add(urlMatcher);
+          } catch (URISyntaxException e) {
+            LOG.invalidURLPattern(param);
+            throw new ServletException(e);
+          }
+
+        }
+
+      }
+    }
   }
 
   @Override
@@ -96,56 +139,110 @@ public class ShiroSubjectIdentityAdapter implements Filter {
       };
       Subject shiroSubject = SecurityUtils.getSubject();
 
+      /**
+       * For cases when we want anonymous authentication to urls in shiro.
+       * This is when do not want authentication for jwks endpoints using
+       * shiro.
+       */
       if (shiroSubject == null || shiroSubject.getPrincipal() == null) {
-        throw new IllegalStateException("Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
-      }
 
-      final String principal = shiroSubject.getPrincipal().toString();
-      Set<Principal> principals = new HashSet<>();
-      Principal p = new PrimaryPrincipal(principal);
-      principals.add(p);
-      AuditContext context = auditService.getContext();
-      context.setUsername( principal );
-      auditService.attachContext(context);
-      String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME );
-      auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS );
-
-      Set<String> userGroups;
-      // map ldap groups saved in session to Java Subject GroupPrincipal(s)
-      if (SecurityUtils.getSubject().getSession().getAttribute(SUBJECT_USER_GROUPS) != null) {
-        userGroups = (Set<String>)SecurityUtils.getSubject().getSession().getAttribute(SUBJECT_USER_GROUPS);
-      } else { // KnoxLdapRealm case
-        if(  shiroSubject.getPrincipal() instanceof String ) {
-           userGroups = new HashSet<>(shiroSubject.getPrincipals().asSet());
-           userGroups.remove(principal);
-        } else { // KnoxPamRealm case
-           Set<Principal> shiroPrincipals = new HashSet<>(shiroSubject.getPrincipals().asSet());
-           userGroups = new HashSet<>(); // Here we are creating a new UserGroup
-                                               // so we don't need to remove a Principal
-                                               // In the case of LDAP the userGroup may have already Principal
-                                               // added to the list of groups, so it is not needed.
-           for( Principal shiroPrincipal: shiroPrincipals ) {
-                userGroups.add(shiroPrincipal.toString() );
-           }
+        if(!isRequestPathInShiroConfig(((HttpServletRequest)request))) {
+          throw new IllegalStateException("Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
         }
+
+        LOG.unauthenticatedPathBypass(
+            ((HttpServletRequest) request).getRequestURI());
+        final String principal = "anonymous";
+        javax.security.auth.Subject subject = new javax.security.auth.Subject();
+        subject.getPrincipals().add(new PrimaryPrincipal(principal));
+        AuditContext context = auditService.getContext();
+        context.setUsername(principal);
+        auditService.attachContext(context);
+        javax.security.auth.Subject.doAs(subject, action);
+      } else {
+
+        final String principal = shiroSubject.getPrincipal().toString();
+        Set<Principal> principals = new HashSet<>();
+        Principal p = new PrimaryPrincipal(principal);
+        principals.add(p);
+        AuditContext context = auditService.getContext();
+        context.setUsername(principal);
+        auditService.attachContext(context);
+        String sourceUri = (String) request.getAttribute(
+            AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME);
+        auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI,
+            ActionOutcome.SUCCESS);
+
+        Set<String> userGroups;
+        // map ldap groups saved in session to Java Subject GroupPrincipal(s)
+        if (SecurityUtils.getSubject().getSession()
+            .getAttribute(SUBJECT_USER_GROUPS) != null) {
+          userGroups = (Set<String>) SecurityUtils.getSubject().getSession()
+              .getAttribute(SUBJECT_USER_GROUPS);
+        } else { // KnoxLdapRealm case
+          if (shiroSubject.getPrincipal() instanceof String) {
+            userGroups = new HashSet<>(shiroSubject.getPrincipals().asSet());
+            userGroups.remove(principal);
+          } else { // KnoxPamRealm case
+            Set<Principal> shiroPrincipals = new HashSet<>(
+                shiroSubject.getPrincipals().asSet());
+            userGroups = new HashSet<>(); // Here we are creating a new UserGroup
+            // so we don't need to remove a Principal
+            // In the case of LDAP the userGroup may have already Principal
+            // added to the list of groups, so it is not needed.
+            for (Principal shiroPrincipal : shiroPrincipals) {
+              userGroups.add(shiroPrincipal.toString());
+            }
+          }
+        }
+        for (String userGroup : userGroups) {
+          Principal gp = new GroupPrincipal(userGroup);
+          principals.add(gp);
+        }
+        auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI,
+            ActionOutcome.SUCCESS, "Groups: " + userGroups);
+
+        // The newly constructed Sets check whether this Subject has been set read-only
+        // before permitting subsequent modifications. The newly created Sets also prevent
+        // illegal modifications by ensuring that callers have sufficient permissions.
+        //
+        // To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals").
+        // To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials").
+        // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials").
+        javax.security.auth.Subject subject = new javax.security.auth.Subject(
+            true, principals, Collections.emptySet(), Collections.emptySet());
+        javax.security.auth.Subject.doAs(subject, action);
       }
-      for (String userGroup : userGroups) {
-        Principal gp = new GroupPrincipal(userGroup);
-        principals.add(gp);
-      }
-      auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS, "Groups: " + userGroups );
-
-      // The newly constructed Sets check whether this Subject has been set read-only
-      // before permitting subsequent modifications. The newly created Sets also prevent
-      // illegal modifications by ensuring that callers have sufficient permissions.
-      //
-      // To modify the Principals Set, the caller must have AuthPermission("modifyPrincipals").
-      // To modify the public credential Set, the caller must have AuthPermission("modifyPublicCredentials").
-      // To modify the private credential Set, the caller must have AuthPermission("modifyPrivateCredentials").
-      javax.security.auth.Subject subject = new javax.security.auth.Subject(true, principals, Collections.emptySet(), Collections.emptySet());
-      javax.security.auth.Subject.doAs( subject, action );
 
       return null;
     }
   }
+
+  /**
+   * A helper function that checks whether the request path is defined in shiro
+   * config, specifically under the urls section with anon authentication. e.g.
+   * <param>
+   * <name>urls./knoxtoken/api/v1/jwks.json</name>
+   * <value>anon</value>
+   * </param>
+   *
+   * @param request
+   * @return true if request has anon auth.
+   * @throws URISyntaxException
+   */
+  private static boolean isRequestPathInShiroConfig(
+      final HttpServletRequest request) throws URISyntaxException {
+    boolean isPathInConfig = false;
+    final String requestContextPath = StringUtils.startsWith(
+        request.getPathInfo(), "/") ?
+        request.getPathInfo() :
+        "/" + request.getPathInfo();
+    final Template requestUrlTemplate = Parser.parseLiteral(requestContextPath);
+    for (final Matcher m : anonUrls) {
+      if (m.match(requestUrlTemplate) != null) {
+        isPathInConfig = true;
+      }
+    }
+    return isPathInConfig;
+  }
 }
diff --git a/gateway-release/home/conf/topologies/sandbox.xml b/gateway-release/home/conf/topologies/sandbox.xml
index d6a4dbcef..646bd8cc2 100644
--- a/gateway-release/home/conf/topologies/sandbox.xml
+++ b/gateway-release/home/conf/topologies/sandbox.xml
@@ -56,6 +56,14 @@
                 <name>main.ldapRealm.contextFactory.authenticationMechanism</name>
                 <value>simple</value>
             </param>
+            <param>
+                <name>urls./knoxtoken/api/v2/jwks.json</name>
+                <value>anon</value>
+            </param>
+            <param>
+                <name>urls./knoxtoken/api/v1/jwks.json</name>
+                <value>anon</value>
+            </param>
             <param>
                 <name>urls./**</name>
                 <value>authcBasic</value>
diff --git a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
index 3a199a53c..ce13a493c 100644
--- a/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
+++ b/gateway-test/src/test/java/org/apache/knox/gateway/GatewayShiroAuthTest.java
@@ -48,12 +48,16 @@ public class GatewayShiroAuthTest {
     private static final Logger LOG = LoggerFactory.getLogger(GatewayShiroAuthTest.class);
     private static final String SHIRO_URL_PATTERN_VALID = "/**";
     private static final String SHIRO_URL_PATTERN_INVALID = "/invalid/**";
+    private static final String SHIRO_URL_PATTERN_JWKS = "/knoxtoken/api/v1/jwks.json";
     private static final String USERNAME = "guest";
     private static final String PASSWORD = "guest-password";
     private static final String TOPOLOGY_VALID = "shiro-test-cluster";
     private static final String TOPOLOGY_IN_VALID_URL = "shiro-test-cluster-invalid";
     private static final String TOPOLOGY_BLOCK_UNSAFE_CHARS = "shiro-test-cluster-unsafe";
+    private static final String TOPOLOGY_JWKS_URL = "shiro-test-jwks";
+    private static final String TOPOLOGY_INVALID_JWKS_URL = "shiro-test-invalid-jwks";
     private static final String SERVICE_RESOURCE_NAME = "/test-service-path/test-service-resource";
+    private static final String SERVICE_TOKEN_STATE_NAME = "/knoxtoken/api/v1/jwks.json";
     public static GatewayConfig config;
     public static GatewayServer gateway;
     public static String gatewayUrl;
@@ -116,6 +120,18 @@ public class GatewayShiroAuthTest {
             createTopology(SHIRO_URL_PATTERN_VALID, true ).toStream(stream);
         }
 
+        /* create a topology with valid shiro url */
+        File jwks_descriptor = new File(topoDir, TOPOLOGY_JWKS_URL+".xml");
+        try (OutputStream stream = Files.newOutputStream(jwks_descriptor.toPath())) {
+            createJWKSTestTopology(SHIRO_URL_PATTERN_JWKS).toStream(stream);
+        }
+
+        /* create a topology with valid shiro url */
+        File jwks_invalid_descriptor = new File(topoDir, TOPOLOGY_INVALID_JWKS_URL+".xml");
+        try (OutputStream stream = Files.newOutputStream(jwks_invalid_descriptor.toPath())) {
+            createJWKSTestTopology("/some-random-path/*").toStream(stream);
+        }
+
         DefaultGatewayServices srvcs = new DefaultGatewayServices();
         Map<String, String> options = new HashMap<>();
         options.put("persist-master", "false");
@@ -215,6 +231,40 @@ public class GatewayShiroAuthTest {
         }
     }
 
+    private static XMLTag createJWKSTestTopology(final String pattern) {
+            return XMLDoc.newDocument(true)
+                .addRoot("topology")
+                .addTag("gateway")
+                .addTag("provider")
+                .addTag("role").addText("authentication")
+                .addTag("name").addText("ShiroProvider")
+                .addTag("enabled").addText("true")
+                .addTag("param")
+                .addTag("name").addText("main.ldapRealm")
+                .addTag("value").addText("org.apache.knox.gateway.shirorealm.KnoxLdapRealm").gotoParent()
+                .addTag("param")
+                .addTag("name").addText("main.ldapRealm.userDnTemplate")
+                .addTag("value").addText("uid={0},ou=people,dc=hadoop,dc=apache,dc=org").gotoParent()
+                .addTag("param")
+                .addTag("name").addText("main.ldapRealm.contextFactory.url")
+                .addTag("value").addText(driver.getLdapUrl()).gotoParent()
+                .addTag("param")
+                .addTag("name").addText("main.ldapRealm.contextFactory.authenticationMechanism")
+                .addTag("value").addText("simple").gotoParent()
+                .addTag("param")
+                .addTag("name").addText("urls." + pattern)
+                .addTag("value").addText("anon").gotoParent().gotoParent()
+                .addTag("provider")
+                .addTag("role").addText("identity-assertion")
+                .addTag("enabled").addText("true")
+                .addTag("name").addText("Default").gotoParent()
+                .addTag("provider")
+                .gotoRoot()
+                .addTag("service")
+                .addTag("role").addText("KNOXTOKEN")
+                .gotoRoot();
+    }
+
     /**
      * Make sure semicolons are allowed in the URL out-of-the-box
      */
@@ -266,4 +316,28 @@ public class GatewayShiroAuthTest {
                 .when().get(serviceUrl);
     }
 
+    /**
+     * Test a case where configured shiro url is invalid and there is auth failure.
+     */
+    @Test
+    public void testJWKSEndpointShiro() {
+        final String serviceUrl = gatewayUrl + '/' + TOPOLOGY_JWKS_URL + SERVICE_TOKEN_STATE_NAME;
+        given()
+            .auth().none()
+            .then()
+            .statusCode(HttpStatus.SC_OK)
+            .contentType("")
+            .body(containsString("{\"keys\":[{\"kty\":\"RSA\""))
+            .when().get(serviceUrl);
+    }
+
+    /**
+     * Test a case where configured shiro url is invalid and there is auth failure.
+     */
+    @Test
+    public void testUnConfiguredJWKSEndpointShiro() {
+        final String serviceUrl = gatewayUrl + '/' + TOPOLOGY_INVALID_JWKS_URL + SERVICE_TOKEN_STATE_NAME;
+        testShiroAuthFailure(HttpStatus.SC_INTERNAL_SERVER_ERROR, serviceUrl, "Unable to determine authenticated user from Shiro, please check that your Knox Shiro configuration is correct");
+    }
+
 }