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

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git

commit a8388778421b3239476049f7295eb4ce2520c432
Author: Jan Høydahl <[email protected]>
AuthorDate: Mon Feb 7 22:26:33 2022 +0100

    SOLR-15907: Establish jwt-auth module
---
 gradle/maven/defaults-maven.gradle                 |   1 +
 gradle/validation/check-broken-links.gradle        |   2 +-
 settings.gradle                                    |   1 +
 solr/CHANGES.txt                                   |   2 +
 solr/benchmark/build.gradle                        |   1 +
 solr/core/build.gradle                             |  18 +----
 .../org/apache/solr/core/SolrResourceLoader.java   |   3 +-
 .../apache/solr/security/AuthenticationPlugin.java |   4 +-
 .../netty-codec-http-4.1.68.Final.jar.sha1         |   1 +
 solr/modules/jwt-auth/README.md                    |  25 +++++++
 solr/modules/jwt-auth/build.gradle                 |  56 +++++++++++++++
 .../apache/solr/security/jwt}/JWTAuthPlugin.java   |  13 ++--
 .../apache/solr/security/jwt}/JWTIssuerConfig.java |  16 ++---
 .../apache/solr/security/jwt}/JWTPrincipal.java    |  15 ++--
 .../security/jwt}/JWTPrincipalWithUserRoles.java   |  22 +++---
 .../security/jwt}/JWTVerificationkeyResolver.java  |  36 +++++-----
 .../apache/solr/security/jwt/package-info.java}    |  21 +-----
 solr/modules/jwt-auth/src/java/overview.html       |  21 ++++++
 .../solr/configsets/cloud-minimal/conf/schema.xml  |  29 ++++++++
 .../configsets/cloud-minimal/conf/solrconfig.xml   |  51 ++++++++++++++
 .../solr/security/jwt_plugin_idp_cert.pem          |   0
 .../solr/security/jwt_plugin_idp_certs.p12         | Bin
 .../solr/security/jwt_plugin_idp_invalidcert.pem   |   0
 .../solr/security/jwt_plugin_idp_wrongcert.pem     |   0
 .../solr/security/jwt_plugin_jwk_security.json     |   0
 .../jwt_plugin_jwk_security_blockUnknownFalse.json |   0
 .../solr/security/jwt_plugin_jwk_url_security.json |   0
 .../solr/security/jwt_well-known-config.json       |   0
 .../jwt}/JWTAuthPluginIntegrationTest.java         |  43 ++++++++----
 .../solr/security/jwt}/JWTAuthPluginTest.java      |  76 +++++++++------------
 .../solr/security/jwt}/JWTIssuerConfigTest.java    |  10 +--
 .../jwt}/JWTVerificationkeyResolverTest.java       |   9 +--
 solr/modules/langid/build.gradle                   |   3 +-
 solr/packaging/build.gradle                        |   1 +
 solr/prometheus-exporter/build.gradle              |   2 +-
 .../src/jwt-authentication-plugin.adoc             |   5 ++
 .../src/major-changes-in-solr-9.adoc               |   3 +
 .../apache/solr/cloud/MiniSolrCloudCluster.java    |  13 +++-
 .../apache/solr/cloud/SolrCloudAuthTestCase.java   |  16 +----
 versions.lock                                      |  11 +--
 40 files changed, 345 insertions(+), 185 deletions(-)

diff --git a/gradle/maven/defaults-maven.gradle 
b/gradle/maven/defaults-maven.gradle
index 278ad68..509b707 100644
--- a/gradle/maven/defaults-maven.gradle
+++ b/gradle/maven/defaults-maven.gradle
@@ -33,6 +33,7 @@ configure(rootProject) {
         ":solr:modules:extraction",
         ":solr:modules:gcs-repository",
         ":solr:modules:jaegertracer-configurator",
+        ":solr:modules:jwt-auth",
         ":solr:modules:langid",
         ":solr:modules:ltr",
         ":solr:modules:s3-repository",
diff --git a/gradle/validation/check-broken-links.gradle 
b/gradle/validation/check-broken-links.gradle
index d032f41..885e1a7 100644
--- a/gradle/validation/check-broken-links.gradle
+++ b/gradle/validation/check-broken-links.gradle
@@ -64,7 +64,7 @@ class CheckBrokenLinksTask extends DefaultTask {
     }
 
     if (result.getExitValue() != 0) {
-      throw new GradleException("Broken links check failed. Command output at: 
${outputFile}")
+      throw new GradleException("Broken links check failed. Command output at: 
${outputFile.get()}")
     }
   }
 }
diff --git a/settings.gradle b/settings.gradle
index 6a78dd4..9a2dcd7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -35,6 +35,7 @@ include "solr:modules:clustering"
 include "solr:modules:extraction"
 include "solr:modules:langid"
 include "solr:modules:jaegertracer-configurator"
+include "solr:modules:jwt-auth"
 include "solr:modules:s3-repository"
 include "solr:modules:scripting"
 include "solr:modules:ltr"
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index b9f96dc..08b37cb 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -535,6 +535,8 @@ Other Changes
 
 * SOLR-15807: New LogListener class for tests to use to make assertions about 
what Log messages should or should not be produced by a test (hossman)
 
+* SOLR-15907: Move JWT Authenticaion plugin to module 'jwt-auth' (janhoy)
+
 * SOLR-15845: Add a new SolrVersion class to manage Solr's version 
independently from the Lucene version we consume (janhoy)
 
 * SOLR-14858: Add the server WEB-INF/lib directory to the classpath for the 
solr-exporter script. Will allow the script
diff --git a/solr/benchmark/build.gradle b/solr/benchmark/build.gradle
index cbfb9d1..8575d36 100644
--- a/solr/benchmark/build.gradle
+++ b/solr/benchmark/build.gradle
@@ -56,6 +56,7 @@ dependencies {
   implementation 'org.jctools:jctools-core'
   implementation 'org.quicktheories:quicktheories'
   implementation 'org.openjdk.jmh:jmh-core'
+  implementation 'org.slf4j:slf4j-api'
   annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
   implementation 'org.slf4j:slf4j-api'
 }
diff --git a/solr/core/build.gradle b/solr/core/build.gradle
index ed43b04..a709f83 100644
--- a/solr/core/build.gradle
+++ b/solr/core/build.gradle
@@ -39,6 +39,9 @@ dependencies {
   api "org.apache.lucene:lucene-analysis-common"
   api "org.apache.lucene:lucene-queries"
 
+  // We export logging api with dependencies, which is useful for all modules
+  api 'org.slf4j:slf4j-api'
+
   api project(':solr:solrj')
   api project(':solr:server')
 
@@ -70,8 +73,6 @@ dependencies {
 
   implementation('com.github.ben-manes.caffeine:caffeine') { transitive = 
false }
 
-  implementation 'org.slf4j:slf4j-api'
-
   implementation 'commons-codec:commons-codec'
 
   implementation 'commons-cli:commons-cli'
@@ -148,9 +149,6 @@ dependencies {
 
   testImplementation 'org.slf4j:jcl-over-slf4j'
 
-  // JWT Auth plugin
-  api 'org.bitbucket.b_c:jose4j'
-
   // For faster XML processing than the JDK
   implementation 'org.codehaus.woodstox:stax2-api'
   implementation 'com.fasterxml.woodstox:woodstox-core'
@@ -204,16 +202,6 @@ dependencies {
   testImplementation('org.mockito:mockito-core', {
     exclude group: "net.bytebuddy", module: "byte-buddy-agent"
   })
-
-  testImplementation('no.nav.security:mock-oauth2-server', {
-    exclude group: "ch.qos.logback", module: "logback-core"
-    exclude group: "io.netty", module: "netty-all"
-    exclude group: "ch.qos.logback", module: "logback-classic"
-  })
-  
-  testImplementation 'com.nimbusds:nimbus-jose-jwt'
-  testImplementation 'com.squareup.okhttp3:mockwebserver'
-  testImplementation 'com.squareup.okhttp3:okhttp'
 }
 
 
diff --git a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java 
b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
index 661b061..2109694 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
@@ -87,7 +87,8 @@ public class SolrResourceLoader implements ResourceLoader, 
Closeable, SolrClassL
   private static final String[] packages = {
       "", "analysis.", "schema.", "handler.", "handler.tagger.", "search.", 
"update.", "core.", "response.", "request.",
       "update.processor.", "util.", "spelling.", "handler.component.",
-      "spelling.suggest.", "spelling.suggest.fst.", "rest.schema.analysis.", 
"security.", "handler.admin."
+      "spelling.suggest.", "spelling.suggest.fst.", "rest.schema.analysis.", 
"security.", "handler.admin.",
+      "security.jwt."
   };
   private static final Charset UTF_8 = StandardCharsets.UTF_8;
   public static final String SOLR_ALLOW_UNSAFE_RESOURCELOADING_PARAM = 
"solr.allow.unsafe.resourceloading";
diff --git 
a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java 
b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
index 0f4442c..b39bc41 100644
--- a/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/AuthenticationPlugin.java
@@ -95,11 +95,11 @@ public abstract class AuthenticationPlugin implements 
SolrInfoBean {
     }
   }
 
-  HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal 
principal) {
+  protected HttpServletRequest wrapWithPrincipal(HttpServletRequest request, 
Principal principal) {
       return wrapWithPrincipal(request, principal, principal.getName());
   }
 
-  HttpServletRequest wrapWithPrincipal(HttpServletRequest request, Principal 
principal, String username) {
+  protected HttpServletRequest wrapWithPrincipal(HttpServletRequest request, 
Principal principal, String username) {
     return new HttpServletRequestWrapper(request) {
       @Override
       public Principal getUserPrincipal() {
diff --git a/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1 
b/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1
new file mode 100644
index 0000000..a05d228
--- /dev/null
+++ b/solr/licenses/netty-codec-http-4.1.68.Final.jar.sha1
@@ -0,0 +1 @@
+fc2e0526ceba7fe1d0ca1adfedc301afcc47bc51
diff --git a/solr/modules/jwt-auth/README.md b/solr/modules/jwt-auth/README.md
new file mode 100644
index 0000000..eb2a275
--- /dev/null
+++ b/solr/modules/jwt-auth/README.md
@@ -0,0 +1,25 @@
+Apache Solr JWT Authentication Plugin
+=====================================
+
+Introduction
+------------
+Solr can support [JSON Web 
Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) based 
+Bearer authentication with the use of the JWTAuthPlugin.
+
+This allows Solr to assert that a user is already authenticated with an 
external 
+[Identity Provider (IdP)](https://en.wikipedia.org/wiki/Identity_provider) by 
validating 
+that the JWT formatted [access 
token](https://en.wikipedia.org/wiki/Access_token) 
+is digitally signed by the Identity Provider.
+
+The typical use case is to integrate Solr with an [OpenID 
Connect](https://en.wikipedia.org/wiki/OpenID_Connect) 
+enabled Identity Provider.
+
+
+Getting Started
+---------------
+Please refer to the Solr Ref Guide at 
https://solr.apache.org/guide/jwt-authentication-plugin.html
+for more information.
+
+User interface
+--------------
+A User interface for this module is part of Solr Core.
\ No newline at end of file
diff --git a/solr/modules/jwt-auth/build.gradle 
b/solr/modules/jwt-auth/build.gradle
new file mode 100644
index 0000000..e747491
--- /dev/null
+++ b/solr/modules/jwt-auth/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'java-library'
+
+description = 'JWT / OpenID Connect / OAuth2 authentication plugin'
+
+dependencies {
+  implementation project(':solr:core')
+  implementation project(':solr:solrj')
+
+  implementation 'org.bitbucket.b_c:jose4j'
+
+  implementation 'commons-io:commons-io'
+  implementation 'io.dropwizard.metrics:metrics-core'
+  implementation 'javax.servlet:javax.servlet-api'
+  implementation 'org.apache.commons:commons-lang3'
+  implementation 'org.apache.httpcomponents:httpclient'
+  implementation 'org.apache.httpcomponents:httpcore'
+  implementation 'org.eclipse.jetty:jetty-client'
+  implementation ('com.google.guava:guava') { transitive = false }
+  implementation 'org.slf4j:slf4j-api'
+
+  implementation 'com.fasterxml.jackson.core:jackson-databind'
+
+  testImplementation project(':solr:test-framework')
+  testImplementation 'org.apache.lucene:lucene-test-framework'
+  testImplementation 'junit:junit'
+
+  testImplementation('org.mockito:mockito-core', {
+    exclude group: "net.bytebuddy", module: "byte-buddy-agent"
+  })
+  testImplementation('no.nav.security:mock-oauth2-server', {
+    exclude group: "ch.qos.logback", module: "logback-core"
+    exclude group: "io.netty", module: "netty-all"
+    exclude group: "ch.qos.logback", module: "logback-classic"
+  })
+  testImplementation 'com.nimbusds:nimbus-jose-jwt'
+  testImplementation 'com.squareup.okhttp3:mockwebserver'
+  testImplementation 'com.squareup.okhttp3:okhttp'
+  testImplementation 'io.netty:netty-codec-http'
+}
diff --git a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
similarity index 99%
rename from solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
rename to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
index ab040bc..83fcb48 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTAuthPlugin.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTAuthPlugin.java
@@ -14,9 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
-import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
@@ -58,7 +57,9 @@ import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.common.util.ValidatingJsonMap;
 import org.apache.solr.core.CoreContainer;
-import 
org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
+import org.apache.solr.security.AuthenticationPlugin;
+import org.apache.solr.security.ConfigEditablePlugin;
+import 
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode;
 import org.apache.solr.util.CryptoKeys;
 import org.eclipse.jetty.client.api.Request;
 import org.jose4j.jwa.AlgorithmConstraints;
@@ -103,7 +104,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
   private static final String PARAM_ALG_WHITELIST = "algWhitelist";
 
   private static final Set<String> PROPS =
-      ImmutableSet.of(
+      Set.of(
           PARAM_BLOCK_UNKNOWN,
           PARAM_PRINCIPAL_CLAIM,
           PARAM_REQUIRE_EXPIRATIONTIME,
@@ -693,7 +694,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
   }
 
   @Override
-  public void close() throws IOException {
+  public void close() {
     jwtConsumer = null;
   }
 
@@ -781,7 +782,7 @@ public class JWTAuthPlugin extends AuthenticationPlugin
   }
 
   /** Response for authentication attempt */
-  static class JWTAuthenticationResponse {
+  protected static class JWTAuthenticationResponse {
     private final Principal principal;
     private String errorMessage;
     private final AuthCode authCode;
diff --git a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
similarity index 97%
rename from solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java
rename to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
index 11b4115..9b3d06a 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTIssuerConfig.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTIssuerConfig.java
@@ -15,13 +15,12 @@
  * limitations under the License.
  */
 
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
 import com.google.common.annotations.VisibleForTesting;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.invoke.MethodHandles;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.charset.Charset;
@@ -43,12 +42,9 @@ import org.jose4j.jwk.HttpsJwks;
 import org.jose4j.jwk.JsonWebKey;
 import org.jose4j.jwk.JsonWebKeySet;
 import org.jose4j.lang.JoseException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /** Holds information about an IdP (issuer), such as issuer ID, JWK url(s), 
keys etc */
 public class JWTIssuerConfig {
-  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   static final String PARAM_ISS_NAME = "name";
   static final String PARAM_JWKS_URL = "jwksUrl";
   static final String PARAM_JWK = "jwk";
@@ -378,8 +374,7 @@ public class JWTIssuerConfig {
     return this.trustedCerts;
   }
 
-  /** */
-  static class HttpsJwksFactory {
+  public static class HttpsJwksFactory {
     private final long jwkCacheDuration;
     private final long refreshReprieveThreshold;
     private Collection<X509Certificate> trustedCerts;
@@ -398,7 +393,7 @@ public class JWTIssuerConfig {
       this.trustedCerts = trustedCerts;
     }
 
-    /**
+    /*
      * While the class name is HttpsJwks, it actually works with plain http 
formatted url as well.
      *
      * @param url the Url to connect to for JWK details.
@@ -433,8 +428,9 @@ public class JWTIssuerConfig {
   }
 
   /**
-   * Config object for a OpenId Connect well-known config Typically exposed 
through
-   * /.well-known/openid-configuration endpoint
+   * Config object for a OpenId Connect well-known config.
+   *
+   * <p>Typically exposed through <code>/.well-known/openid-configuration 
endpoint</code>.
    */
   public static class WellKnownDiscoveryConfig {
     private final Map<String, Object> securityConf;
diff --git a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java
similarity index 91%
rename from solr/core/src/java/org/apache/solr/security/JWTPrincipal.java
rename to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java
index a779fad..ec70bb2 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTPrincipal.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipal.java
@@ -15,9 +15,10 @@
  * limitations under the License.
  */
 
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
 import java.security.Principal;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.http.util.Args;
@@ -75,15 +76,7 @@ public class JWTPrincipal implements Principal {
 
   @Override
   public String toString() {
-    return "JWTPrincipal{"
-        + "username='"
-        + username
-        + '\''
-        + ", token='"
-        + "*****"
-        + '\''
-        + ", claims="
-        + claims
-        + '}';
+    return String.format(
+        Locale.ROOT, "JWTPrincipal{username='%s', token='*****', claims=%s}", 
username, claims);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java
similarity index 87%
rename from 
solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
rename to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java
index 856e6c2..1f298a1 100644
--- a/solr/core/src/java/org/apache/solr/security/JWTPrincipalWithUserRoles.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTPrincipalWithUserRoles.java
@@ -15,12 +15,14 @@
  * limitations under the License.
  */
 
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.http.util.Args;
+import org.apache.solr.security.VerifiedUserRoles;
 
 /**
  * JWT principal that contains username, token, claims and a list of roles the 
user has, so one can
@@ -58,17 +60,11 @@ public class JWTPrincipalWithUserRoles extends JWTPrincipal 
implements VerifiedU
 
   @Override
   public String toString() {
-    return "JWTPrincipalWithUserRoles{"
-        + "username='"
-        + username
-        + '\''
-        + ", token='"
-        + "*****"
-        + '\''
-        + ", claims="
-        + claims
-        + ", roles="
-        + roles
-        + '}';
+    return String.format(
+        Locale.ROOT,
+        "JWTPrincipalWithUserRoles{username='%s', token='*****', claims=%s, 
roles=%s}",
+        username,
+        claims,
+        roles);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java
similarity index 88%
rename from 
solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java
rename to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java
index 08e5cb5..dd7a54b 100644
--- 
a/solr/core/src/java/org/apache/solr/security/JWTVerificationkeyResolver.java
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/JWTVerificationkeyResolver.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
@@ -24,6 +24,7 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import javax.net.ssl.SSLHandshakeException;
@@ -154,25 +155,26 @@ public class JWTVerificationkeyResolver implements 
VerificationKeyResolver {
         theChosenOne = verificationJwkSelector.select(jws, jsonWebKeys);
       }
     } catch (JoseException | IOException | InvalidJwtException | 
MalformedClaimException e) {
-      StringBuilder sb = new StringBuilder();
-      sb.append("Unable to find a suitable verification key for JWS w/ header 
")
-          .append(jws.getHeaders().getFullHeaderAsJsonString());
-      sb.append(" due to an unexpected exception (")
-          .append(e)
-          .append(") while obtaining or using keys from source ");
-      sb.append(keysSource);
-      throw new UnresolvableKeyException(sb.toString(), e);
+      String msg =
+          String.format(
+              Locale.ROOT,
+              "Unable to find a suitable verification key for JWS w/ header %s 
due to an unexpected exception (%s) "
+                  + "while obtaining or using keys from source %s",
+              jws.getHeaders().getFullHeaderAsJsonString(),
+              e,
+              keysSource);
+      throw new UnresolvableKeyException(msg, e);
     }
 
     if (theChosenOne == null) {
-      StringBuilder sb = new StringBuilder();
-      sb.append("Unable to find a suitable verification key for JWS w/ header 
")
-          .append(jws.getHeaders().getFullHeaderAsJsonString());
-      sb.append(" from ")
-          .append(jsonWebKeys.size())
-          .append(" keys from source ")
-          .append(keysSource);
-      throw new UnresolvableKeyException(sb.toString());
+      String msg =
+          String.format(
+              Locale.ROOT,
+              "Unable to find a suitable verification key for JWS w/ header %s 
from %d keys from source %s",
+              jws.getHeaders().getFullHeaderAsJsonString(),
+              jsonWebKeys.size(),
+              keysSource);
+      throw new UnresolvableKeyException(msg);
     }
 
     return theChosenOne.getKey();
diff --git a/solr/modules/langid/build.gradle 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java
similarity index 54%
copy from solr/modules/langid/build.gradle
copy to 
solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java
index 5576d9d..1cf2ccd 100644
--- a/solr/modules/langid/build.gradle
+++ 
b/solr/modules/jwt-auth/src/java/org/apache/solr/security/jwt/package-info.java
@@ -15,22 +15,5 @@
  * limitations under the License.
  */
 
-apply plugin: 'java-library'
-
-description = 'Language Identifier module for extracting language from a 
document being indexed'
-
-dependencies {
-  implementation project(':solr:core')
-  implementation project(':solr:solrj')
-
-  implementation ('org.apache.tika:tika-core') { transitive = false }
-  implementation 'commons-io:commons-io'
-  implementation 'com.cybozu.labs:langdetect'
-  implementation 'net.arnx:jsonic'
-  implementation 'org.apache.opennlp:opennlp-tools'
-  implementation 'org.slf4j:slf4j-api'
-
-  testImplementation project(':solr:test-framework')
-  testImplementation 
'com.carrotsearch.randomizedtesting:randomizedtesting-runner'
-  testImplementation 'junit:junit'
-}
+/** JWT authentication plugin */
+package org.apache.solr.security.jwt;
diff --git a/solr/modules/jwt-auth/src/java/overview.html 
b/solr/modules/jwt-auth/src/java/overview.html
new file mode 100644
index 0000000..69fcc17
--- /dev/null
+++ b/solr/modules/jwt-auth/src/java/overview.html
@@ -0,0 +1,21 @@
+<!--
+ 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.
+-->
+<html>
+<body>
+Apache Solr Search Server: Solr Oauth JWT Authentication plugin
+</body>
+</html>
diff --git 
a/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml
 
b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml
new file mode 100644
index 0000000..4124fea
--- /dev/null
+++ 
b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/schema.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<schema name="minimal" version="1.1">
+  <fieldType name="string" class="solr.StrField"/>
+  <fieldType name="int" class="${solr.tests.IntegerFieldType}" 
docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" 
positionIncrementGap="0"/>
+  <fieldType name="long" class="${solr.tests.LongFieldType}" 
docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" 
positionIncrementGap="0"/>
+  <dynamicField name="*" type="string" indexed="true" stored="true"/>
+  <!-- for versioning -->
+  <field name="_version_" type="long" indexed="true" stored="true"/>
+  <field name="_root_" type="string" indexed="true" stored="true" 
multiValued="false" required="false"/>
+  <field name="id" type="string" indexed="true" stored="true"/>
+  <dynamicField name="*_s"  type="string"  indexed="true"  stored="true" />
+  <uniqueKey>id</uniqueKey>
+</schema>
diff --git 
a/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
 
b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
new file mode 100644
index 0000000..853ba65
--- /dev/null
+++ 
b/solr/modules/jwt-auth/src/test-files/solr/configsets/cloud-minimal/conf/solrconfig.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<!-- Minimal solrconfig.xml with /select, /admin and /update only -->
+
+<config>
+
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <directoryFactory name="DirectoryFactory"
+                    
class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+  <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+  <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+    <commitWithin>
+      <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+    </commitWithin>
+    <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog>
+  </updateHandler>
+
+  <requestHandler name="/select" class="solr.SearchHandler">
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+      <str name="indent">true</str>
+      <str name="df">text</str>
+    </lst>
+
+  </requestHandler>
+  <indexConfig>
+    <mergeScheduler 
class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+:  </indexConfig>
+</config>
+
diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_cert.pem 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_cert.pem
similarity index 100%
rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_cert.pem
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_cert.pem
diff --git a/solr/core/src/test-files/solr/security/jwt_plugin_idp_certs.p12 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_certs.p12
similarity index 100%
rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_certs.p12
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_certs.p12
diff --git 
a/solr/core/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem
similarity index 100%
rename from 
solr/core/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_invalidcert.pem
diff --git 
a/solr/core/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem
similarity index 100%
rename from solr/core/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_idp_wrongcert.pem
diff --git 
a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json
similarity index 100%
rename from solr/core/src/test-files/solr/security/jwt_plugin_jwk_security.json
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security.json
diff --git 
a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
similarity index 100%
rename from 
solr/core/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_security_blockUnknownFalse.json
diff --git 
a/solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
similarity index 100%
rename from 
solr/core/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_plugin_jwk_url_security.json
diff --git a/solr/core/src/test-files/solr/security/jwt_well-known-config.json 
b/solr/modules/jwt-auth/src/test-files/solr/security/jwt_well-known-config.json
similarity index 100%
rename from solr/core/src/test-files/solr/security/jwt_well-known-config.json
rename to 
solr/modules/jwt-auth/src/test-files/solr/security/jwt_well-known-config.json
diff --git 
a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
similarity index 94%
rename from 
solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
rename to 
solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
index 434469b..e079d3a 100644
--- 
a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginIntegrationTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginIntegrationTest.java
@@ -14,9 +14,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.solr.security.jwt.JWTAuthPluginTest.JWT_TEST_PATH;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -72,6 +73,7 @@ import org.jose4j.jwk.RsaJwkGenerator;
 import org.jose4j.jws.AlgorithmIdentifiers;
 import org.jose4j.jws.JsonWebSignature;
 import org.jose4j.jwt.JwtClaims;
+import org.jose4j.lang.JoseException;
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
@@ -100,11 +102,9 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
   @BeforeClass
   public static void beforeClass() throws Exception {
     // Setup an OAuth2 mock server with SSL
-    Path p12Cert =
-        
SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12");
-    pemFilePath = 
SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem");
-    wrongPemFilePath =
-        
SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem");
+    Path p12Cert = 
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_certs.p12");
+    pemFilePath = 
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem");
+    wrongPemFilePath = 
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_wrongcert.pem");
 
     mockOAuth2Server = createMockOAuthServer(p12Cert, "secret");
     mockOAuth2Server.start();
@@ -215,13 +215,24 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     assertAuthMetricsMinimums(2, 1, 0, 0, 1, 0);
     executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: 
false}}", jws);
     verifySecurityStatus(
-        cl, baseUrl + authcPrefix, "authentication/blockUnknown", "false", 20, 
jws);
+        cl,
+        baseUrl + authcPrefix,
+        "authentication/blockUnknown",
+        "false",
+        20,
+        getBearerAuthHeader(jws));
     // Pass through
     verifySecurityStatus(cl, baseUrl + "/admin/info/key", "key", 
NOT_NULL_PREDICATE, 20);
     // Now succeeds since blockUnknown=false
     get(baseUrl + "/" + COLLECTION + "/query?q=*:*", null);
     executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: 
true}}", null);
-    verifySecurityStatus(cl, baseUrl + authcPrefix, 
"authentication/blockUnknown", "true", 20, jws);
+    verifySecurityStatus(
+        cl,
+        baseUrl + authcPrefix,
+        "authentication/blockUnknown",
+        "true",
+        20,
+        getBearerAuthHeader(jws));
 
     assertAuthMetricsMinimums(9, 4, 4, 0, 1, 0);
 
@@ -268,6 +279,10 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     HttpClientUtil.close(cl);
   }
 
+  static String getBearerAuthHeader(JsonWebSignature jws) throws JoseException 
{
+    return "Bearer " + jws.getCompactSerialization();
+  }
+
   /**
    * Configure solr cluster with a security.json talking to MockOAuth2 server
    *
@@ -282,13 +297,12 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
     MiniSolrCloudCluster myCluster =
         configureCluster(numNodes) // nodes
             .addConfig(
-                "conf1", 
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
+                "conf1",
+                
JWT_TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
             .withDefaultClusterProperty("useLegacyReplicaAssignment", "false")
             .build();
     String securityJson = createMockOAuthSecurityJson(pemFilePath);
-    myCluster
-        .getZkClient()
-        .setData("/security.json", 
securityJson.getBytes(Charset.defaultCharset()), true);
+    myCluster.zkSetData("/security.json", 
securityJson.getBytes(Charset.defaultCharset()), true);
     RTimer timer = new RTimer();
     do { // Wait timeoutMs time for the security.json change to take effect
       Thread.sleep(200);
@@ -311,9 +325,10 @@ public class JWTAuthPluginIntegrationTest extends 
SolrCloudAuthTestCase {
       throws Exception {
     MiniSolrCloudCluster myCluster =
         configureCluster(2) // nodes
-            
.withSecurityJson(TEST_PATH().resolve("security").resolve(securityJsonFilename))
+            
.withSecurityJson(JWT_TEST_PATH().resolve("security").resolve(securityJsonFilename))
             .addConfig(
-                "conf1", 
TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
+                "conf1",
+                
JWT_TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
             .withDefaultClusterProperty("useLegacyReplicaAssignment", "false")
             .build();
 
diff --git a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
similarity index 89%
rename from solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
rename to 
solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
index 2e1c241..e25d2fb 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTAuthPluginTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTAuthPluginTest.java
@@ -14,9 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
-import static 
org.apache.solr.security.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*;
+import static 
org.apache.solr.security.jwt.JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.*;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -38,6 +38,7 @@ import org.apache.commons.io.IOUtils;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.Utils;
+import org.apache.solr.security.VerifiedUserRoles;
 import org.apache.solr.util.CryptoKeys;
 import org.jose4j.jwk.RsaJsonWebKey;
 import org.jose4j.jwk.RsaJwkGenerator;
@@ -63,6 +64,10 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
   // Shared with other tests
   static HashMap<String, Object> testJwk;
 
+  public static Path JWT_TEST_PATH() {
+    return getFile("solr/security").getParentFile().toPath();
+  }
+
   static {
     // Generate an RSA key pair, which will be used for signing and 
verification of the JWT, wrapped
     // in a JWK
@@ -82,7 +87,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
           "n", 
BigEndianBigInteger.toBase64Url(rsaJsonWebKey.getRsaPublicKey().getModulus()));
 
       trustedPemCert =
-          
Files.readString(TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"));
+          
Files.readString(JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem"));
     } catch (JoseException | IOException e) {
       fail("Failed static initialization: " + e.getMessage());
     }
@@ -142,13 +147,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     plugin = new JWTAuthPlugin();
 
     testConfig = new HashMap<>();
-    testConfig.put("class", "org.apache.solr.security.JWTAuthPlugin");
+    testConfig.put("class", "org.apache.solr.security.jwt.JWTAuthPlugin");
     testConfig.put("principalClaim", "customPrincipal");
     testConfig.put("jwk", testJwk);
     plugin.init(testConfig);
 
     minimalConfig = new HashMap<>();
-    minimalConfig.put("class", "org.apache.solr.security.JWTAuthPlugin");
+    minimalConfig.put("class", "org.apache.solr.security.jwt.JWTAuthPlugin");
   }
 
   @Override
@@ -169,7 +174,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
 
   @Test
   public void initFromSecurityJSONLocalJWK() throws Exception {
-    Path securityJson = 
TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json");
+    Path securityJson = 
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_security.json");
     InputStream is = Files.newInputStream(securityJson);
     Map<String, Object> securityConf = (Map<String, Object>) 
Utils.fromJSON(is);
     Map<String, Object> authConf = (Map<String, Object>) 
securityConf.get("authentication");
@@ -178,16 +183,15 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
 
   @Test
   public void initFromSecurityJSONUrlJwk() throws Exception {
-    Path securityJson = 
TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_url_security.json");
+    Path securityJson =
+        
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_jwk_url_security.json");
     InputStream is = Files.newInputStream(securityJson);
     Map<String, Object> securityConf = (Map<String, Object>) 
Utils.fromJSON(is);
     Map<String, Object> authConf = (Map<String, Object>) 
securityConf.get("authentication");
     plugin.init(authConf);
 
     JWTAuthPlugin.JWTAuthenticationResponse resp = 
plugin.authenticate(testHeader);
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
     assertTrue(resp.getJwtException().getMessage().contains("Connection 
refused"));
   }
 
@@ -236,9 +240,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     plugin.init(minimalConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = 
plugin.authenticate(testHeader);
     assertFalse(resp.isAuthenticated());
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
 
     testConfig.put("principalClaim", "sub"); // testConfig has subject = 
solruser
     plugin.init(testConfig);
@@ -252,9 +254,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     plugin.init(testConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = 
plugin.authenticate(testHeader);
     assertFalse(resp.isAuthenticated());
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
 
     testConfig.put("iss", "IDServer");
     plugin.init(testConfig);
@@ -268,9 +268,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     plugin.init(testConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = 
plugin.authenticate(testHeader);
     assertFalse(resp.isAuthenticated());
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
 
     testConfig.put("aud", "Solr");
     plugin.init(testConfig);
@@ -289,8 +287,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     plugin.init(testConfig);
     resp = plugin.authenticate(testHeader);
     assertFalse(resp.isAuthenticated());
-    assertEquals(
-        JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PRINCIPAL_MISSING, 
resp.getAuthCode());
+    assertEquals(PRINCIPAL_MISSING, resp.getAuthCode());
   }
 
   @Test
@@ -310,15 +307,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     shouldMatch.put("claim9", "NA");
     plugin.init(testConfig);
     resp = plugin.authenticate(testHeader);
-    assertEquals(
-        JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, 
resp.getAuthCode());
+    assertEquals(CLAIM_MISMATCH, resp.getAuthCode());
 
     // Required claim does not match regex
     shouldMatch.clear();
     shouldMatch.put("claim1", "NA");
     resp = plugin.authenticate(testHeader);
-    assertEquals(
-        JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.CLAIM_MISMATCH, 
resp.getAuthCode());
+    assertEquals(CLAIM_MISMATCH, resp.getAuthCode());
   }
 
   @Test
@@ -333,18 +328,14 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     testConfig.put("requireExp", true);
     plugin.init(testConfig);
     resp = plugin.authenticate(slimHeader);
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
     testConfig.put("requireExp", false);
 
     // Missing issuer claim
     testConfig.put("requireIss", true);
     plugin.init(testConfig);
     resp = plugin.authenticate(slimHeader);
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
   }
 
   @Test
@@ -352,9 +343,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     testConfig.put("algAllowlist", Arrays.asList("PS384", "PS512"));
     plugin.init(testConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = 
plugin.authenticate(testHeader);
-    assertEquals(
-        
JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.JWT_VALIDATION_EXCEPTION,
-        resp.getAuthCode());
+    assertEquals(JWT_VALIDATION_EXCEPTION, resp.getAuthCode());
     assertTrue(resp.getErrorMessage().contains("not a permitted algorithm"));
   }
 
@@ -413,7 +402,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     testConfig.put("blockUnknown", false);
     plugin.init(testConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
-    
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, 
resp.getAuthCode());
+    assertEquals(PASS_THROUGH, resp.getAuthCode());
   }
 
   @Test
@@ -421,13 +410,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     minimalConfig.put("blockUnknown", false);
     plugin.init(minimalConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
-    
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, 
resp.getAuthCode());
+    assertEquals(PASS_THROUGH, resp.getAuthCode());
   }
 
   @Test
   public void wellKnownConfigNoHeaderPassThrough() {
     String wellKnownUrl =
-        TEST_PATH()
+        JWT_TEST_PATH()
             .resolve("security")
             .resolve("jwt_well-known-config.json")
             .toAbsolutePath()
@@ -437,13 +426,13 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     testConfig.remove("jwk");
     plugin.init(testConfig);
     JWTAuthPlugin.JWTAuthenticationResponse resp = plugin.authenticate(null);
-    
assertEquals(JWTAuthPlugin.JWTAuthenticationResponse.AuthCode.PASS_THROUGH, 
resp.getAuthCode());
+    assertEquals(PASS_THROUGH, resp.getAuthCode());
   }
 
   @Test
   public void defaultRealm() {
     String wellKnownUrl =
-        TEST_PATH()
+        JWT_TEST_PATH()
             .resolve("security")
             .resolve("jwt_well-known-config.json")
             .toAbsolutePath()
@@ -458,7 +447,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
   @Test
   public void configureRealm() {
     String wellKnownUrl =
-        TEST_PATH()
+        JWT_TEST_PATH()
             .resolve("security")
             .resolve("jwt_well-known-config.json")
             .toAbsolutePath()
@@ -560,7 +549,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk";);
     authConf.put(
         "trustedCertsFile",
-        
TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString());
+        
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem").toString());
     plugin = new JWTAuthPlugin();
     plugin.init(authConf);
     assertEquals(2, plugin.getIssuerConfigs().get(0).getTrustedCerts().size());
@@ -581,7 +570,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
     authConf.put("jwksUrl", "https://127.0.0.1:9999/foo.jwk";);
     authConf.put(
         "trustedCertsFile",
-        
TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString());
+        
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_invalidcert.pem").toString());
     plugin = new JWTAuthPlugin();
     expectThrows(SolrException.class, () -> plugin.init(authConf));
   }
@@ -622,8 +611,7 @@ public class JWTAuthPluginTest extends SolrTestCaseJ4 {
 
   @Test
   public void extractCertificate() throws IOException {
-    Path pemFilePath =
-        
SolrTestCaseJ4.TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem");
+    Path pemFilePath = 
JWT_TEST_PATH().resolve("security").resolve("jwt_plugin_idp_cert.pem");
     String cert = 
CryptoKeys.extractCertificateFromPem(Files.readString(pemFilePath));
     assertEquals(
         2, CryptoKeys.parseX509Certs(IOUtils.toInputStream(cert, 
StandardCharsets.UTF_8)).size());
diff --git 
a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
similarity index 95%
rename from solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java
rename to 
solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
index f15f4e4..d51ae82 100644
--- a/solr/core/src/test/org/apache/solr/security/JWTIssuerConfigTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTIssuerConfigTest.java
@@ -15,10 +15,10 @@
  * limitations under the License.
  */
 
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
-import static org.apache.solr.SolrTestCaseJ4.TEST_PATH;
-import static org.apache.solr.security.JWTAuthPluginTest.testJwk;
+import static org.apache.solr.security.jwt.JWTAuthPluginTest.JWT_TEST_PATH;
+import static org.apache.solr.security.jwt.JWTAuthPluginTest.testJwk;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
@@ -157,7 +157,7 @@ public class JWTIssuerConfigTest extends SolrTestCase {
 
   @Test
   public void wellKnownConfigFromInputstream() throws IOException {
-    Path configJson = 
TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
+    Path configJson = 
JWT_TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
     JWTIssuerConfig.WellKnownDiscoveryConfig config =
         
JWTIssuerConfig.WellKnownDiscoveryConfig.parse(Files.newInputStream(configJson));
     assertEquals("https://acmepaymentscorp/oauth/jwks";, config.getJwksUrl());
@@ -165,7 +165,7 @@ public class JWTIssuerConfigTest extends SolrTestCase {
 
   @Test
   public void wellKnownConfigFromString() throws IOException {
-    Path configJson = 
TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
+    Path configJson = 
JWT_TEST_PATH().resolve("security").resolve("jwt_well-known-config.json");
     String configString = StringUtils.join(Files.readAllLines(configJson), 
"\n");
     JWTIssuerConfig.WellKnownDiscoveryConfig config =
         JWTIssuerConfig.WellKnownDiscoveryConfig.parse(configString, 
StandardCharsets.UTF_8);
diff --git 
a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java
 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
similarity index 95%
rename from 
solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java
rename to 
solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
index c24b9d8..3de70e6 100644
--- 
a/solr/core/src/test/org/apache/solr/security/JWTVerificationkeyResolverTest.java
+++ 
b/solr/modules/jwt-auth/src/test/org/apache/solr/security/jwt/JWTVerificationkeyResolverTest.java
@@ -15,10 +15,9 @@
  * limitations under the License.
  */
 
-package org.apache.solr.security;
+package org.apache.solr.security.jwt;
 
 import static java.util.Arrays.asList;
-import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.when;
 
@@ -26,7 +25,7 @@ import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.security.JWTIssuerConfig.HttpsJwksFactory;
+import org.apache.solr.security.jwt.JWTIssuerConfig.HttpsJwksFactory;
 import org.jose4j.jwk.HttpsJwks;
 import org.jose4j.jwk.JsonWebKey;
 import org.jose4j.jwk.RsaJsonWebKey;
@@ -38,6 +37,7 @@ import org.jose4j.lang.UnresolvableKeyException;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -85,7 +85,8 @@ public class JWTVerificationkeyResolverTest extends 
SolrTestCaseJ4 {
                 keysToReturnFromSecondJwk = refreshSequenceForSecondJwk.next();
               return keysToReturnFromSecondJwk;
             });
-    
when(httpsJwksFactory.createList(anyList())).thenReturn(asList(firstJwkList, 
secondJwkList));
+    when(httpsJwksFactory.createList(ArgumentMatchers.anyList()))
+        .thenReturn(asList(firstJwkList, secondJwkList));
 
     JWTIssuerConfig issuerConfig =
         new JWTIssuerConfig("primary").setIss("foo").setJwksUrl(asList("url1", 
"url2"));
diff --git a/solr/modules/langid/build.gradle b/solr/modules/langid/build.gradle
index 5576d9d..a88a202 100644
--- a/solr/modules/langid/build.gradle
+++ b/solr/modules/langid/build.gradle
@@ -28,8 +28,7 @@ dependencies {
   implementation 'com.cybozu.labs:langdetect'
   implementation 'net.arnx:jsonic'
   implementation 'org.apache.opennlp:opennlp-tools'
-  implementation 'org.slf4j:slf4j-api'
-
+implementation 'org.slf4j:slf4j-api'
   testImplementation project(':solr:test-framework')
   testImplementation 
'com.carrotsearch.randomizedtesting:randomizedtesting-runner'
   testImplementation 'junit:junit'
diff --git a/solr/packaging/build.gradle b/solr/packaging/build.gradle
index 40006f0..da324d8 100644
--- a/solr/packaging/build.gradle
+++ b/solr/packaging/build.gradle
@@ -53,6 +53,7 @@ dependencies {
    ":solr:modules:gcs-repository",
    ":solr:modules:hdfs",
    ":solr:modules:jaegertracer-configurator",
+   ":solr:modules:jwt-auth",
    ":solr:modules:langid",
    ":solr:modules:ltr",
    ":solr:modules:s3-repository",
diff --git a/solr/prometheus-exporter/build.gradle 
b/solr/prometheus-exporter/build.gradle
index ecd1437..9402fce 100644
--- a/solr/prometheus-exporter/build.gradle
+++ b/solr/prometheus-exporter/build.gradle
@@ -22,7 +22,6 @@ description = 'Prometheus exporter for exposing metrics from 
Solr using Metrics
 
 dependencies {
   implementation project(':solr:solrj')
-  implementation 'org.slf4j:slf4j-api'
   implementation('org.apache.zookeeper:zookeeper') { transitive = false } // 
ideally remove ZK dep
 
   implementation ('io.prometheus:simpleclient')
@@ -36,6 +35,7 @@ dependencies {
   implementation ('net.sourceforge.argparse4j:argparse4j')
   implementation ('com.github.ben-manes.caffeine:caffeine') { transitive = 
false }
   implementation 'commons-io:commons-io'
+  implementation 'org.slf4j:slf4j-api'
 
   runtimeOnly 'org.apache.logging.log4j:log4j-api'
   runtimeOnly 'org.apache.logging.log4j:log4j-core'
diff --git a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc 
b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
index bcf3d72..c928e60 100644
--- a/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
+++ b/solr/solr-ref-guide/src/jwt-authentication-plugin.adoc
@@ -21,6 +21,11 @@ Solr can support 
https://en.wikipedia.org/wiki/JSON_Web_Token[JSON Web Token] (J
 This allows Solr to assert that a user is already authenticated with an 
external https://en.wikipedia.org/wiki/Identity_provider[Identity Provider] by 
validating that the JWT formatted 
https://en.wikipedia.org/wiki/Access_token[access token] is digitally signed by 
the Identity Provider.
 The typical use case is to integrate Solr with an 
https://en.wikipedia.org/wiki/OpenID_Connect[OpenID Connect] enabled IdP.
 
+== Module
+
+This is provided via a Solr Module that needs to be added to the classpath 
before use. Since this is a node-level
+plugin it must go in `sharedLib`, see <<configuring-solr-xml.adoc#,Configuring 
solr.xml>> for details.
+
 == Enable JWT Authentication
 
 To use JWT Bearer authentication, the `security.json` file must have an 
`authentication` part which defines the class being used for authentication 
along with configuration parameters.
diff --git a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc 
b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
index 17611da..fbc5f8e 100644
--- a/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
+++ b/solr/solr-ref-guide/src/major-changes-in-solr-9.adoc
@@ -110,6 +110,9 @@ This is only applicable for users returning information in 
JSON format, which is
 * SOLR-14660: HDFS storage support has been moved to a module. Existing Solr 
configurations do not need any HDFS-related
 changes, however the module needs to be installed - see 
<<solr-on-hdfs.adoc#,Running Solr on HDFS>>.
 
+* SOLR-15097: JWTAuthPlugin has been moved to a module. Users need to add the 
module to classpath. The plugin has also
+  changed package name to `org.apache.solr.security.jwt`, but can still be 
loaded as shortform `class="solr.JWTAuthPlugin"`.
+
 == New Features & Enhancements
 
 * Replica placement plugins
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java 
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
index aa9e533..d11d22e 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/MiniSolrCloudCluster.java
@@ -644,7 +644,18 @@ public class MiniSolrCloudCluster {
   public SolrZkClient getZkClient() {
     return solrClient.getZkStateReader().getZkClient();
   }
-  
+
+  /**
+   * Set data in zk without exposing caller to the ZK API, i.e. tests won't 
need to include Zookeeper dependencies
+   */
+  public void zkSetData(String path, byte[] data, boolean retryOnConnLoss) 
throws InterruptedException {
+    try {
+      getZkClient().setData(path, data, -1, retryOnConnLoss);
+    } catch (KeeperException e) {
+      throw new SolrException(ErrorCode.UNKNOWN, "Failed writing to 
Zookeeper", e);
+    }
+  }
+
   protected CloudSolrClient buildSolrClient() {
     return new 
CloudSolrClient.Builder(Collections.singletonList(getZkServer().getZkAddress()),
 Optional.empty())
         .withSocketTimeout(90000).withConnectionTimeout(15000).build(); // we 
choose 90 because we run in some harsh envs
diff --git 
a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java 
b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
index f918562..b8ff1e8 100644
--- 
a/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
+++ 
b/solr/test-framework/src/java/org/apache/solr/cloud/SolrCloudAuthTestCase.java
@@ -44,8 +44,6 @@ import org.apache.solr.client.solrj.embedded.JettySolrRunner;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
 import org.apache.solr.util.TimeOut;
-import org.jose4j.jws.JsonWebSignature;
-import org.jose4j.lang.JoseException;
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.slf4j.Logger;
@@ -195,15 +193,9 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
     verifySecurityStatus(cl, url, objPath, expected, count, 
makeBasicAuthHeader(user, pwd));
   }
 
-  protected void verifySecurityStatus(HttpClient cl, String url, String 
objPath,
-                                      Object expected, int count, 
JsonWebSignature jws) throws Exception {
-    verifySecurityStatus(cl, url, objPath, expected, count, 
getBearerAuthHeader(jws));
-  }
-
-
   @SuppressWarnings({"unchecked"})
-  private static void verifySecurityStatus(HttpClient cl, String url, String 
objPath,
-                                            Object expected, int count, String 
authHeader) throws IOException, InterruptedException {
+  protected static void verifySecurityStatus(HttpClient cl, String url, String 
objPath,
+                                             Object expected, int count, 
String authHeader) throws IOException, InterruptedException {
     boolean success = false;
     String s = null;
     List<String> hierarchy = StrUtils.splitSmart(objPath, '/');
@@ -242,10 +234,6 @@ public class SolrCloudAuthTestCase extends 
SolrCloudTestCase {
     return "Basic " + 
Base64.getEncoder().encodeToString(userPass.getBytes(UTF_8));
   }
 
-  static String getBearerAuthHeader(JsonWebSignature jws) throws JoseException 
{
-    return "Bearer " + jws.getCompactSerialization();
-  }
-  
   public static void setAuthorizationHeader(AbstractHttpMessage httpMsg, 
String headerString) {
     httpMsg.setHeader(new BasicHeader("Authorization", headerString));
     log.info("Added Authorization Header {}", headerString);
diff --git a/versions.lock b/versions.lock
index b673d06..14d5eea 100644
--- a/versions.lock
+++ b/versions.lock
@@ -70,12 +70,12 @@ io.dropwizard.metrics:metrics-jvm:4.1.5 (1 constraints: 
0c050736)
 io.grpc:grpc-context:1.36.0 (3 constraints: 5a22231e)
 io.jaegertracing:jaeger-core:1.6.0 (2 constraints: 6912c420)
 io.jaegertracing:jaeger-thrift:1.6.0 (1 constraints: 09050236)
-io.netty:netty-buffer:4.1.68.Final (5 constraints: df4da774)
-io.netty:netty-codec:4.1.68.Final (1 constraints: a60ccb09)
-io.netty:netty-common:4.1.68.Final (7 constraints: 59675dcf)
-io.netty:netty-handler:4.1.68.Final (1 constraints: db0f158f)
+io.netty:netty-buffer:4.1.68.Final (6 constraints: 915bfcb7)
+io.netty:netty-codec:4.1.68.Final (2 constraints: 581a4d48)
+io.netty:netty-common:4.1.68.Final (8 constraints: 0b75841f)
+io.netty:netty-handler:4.1.68.Final (2 constraints: 8d1d363b)
 io.netty:netty-resolver:4.1.68.Final (2 constraints: 5a1a4732)
-io.netty:netty-transport:4.1.68.Final (4 constraints: 2b402b76)
+io.netty:netty-transport:4.1.68.Final (5 constraints: dd4d9295)
 io.netty:netty-transport-native-epoll:4.1.68.Final (1 constraints: db0f158f)
 io.netty:netty-transport-native-unix-common:4.1.68.Final (1 constraints: 
b212fc1e)
 io.opencensus:opencensus-api:0.28.0 (5 constraints: 21426a37)
@@ -295,6 +295,7 @@ 
com.vaadin.external.google:android-json:0.0.20131108.vaadin1 (1 constraints: 340
 io.github.microutils:kotlin-logging:2.0.6 (1 constraints: be0e9d62)
 io.github.microutils:kotlin-logging-jvm:2.0.6 (1 constraints: 810f877c)
 io.micrometer:micrometer-core:1.5.14 (1 constraints: fc161b19)
+io.netty:netty-codec-http:4.1.68.Final (1 constraints: 5d077961)
 io.opentracing:opentracing-mock:0.33.0 (1 constraints: 3805343b)
 jakarta.activation:jakarta.activation-api:1.2.1 (2 constraints: b928fbbd)
 jakarta.annotation:jakarta.annotation-api:1.3.5 (1 constraints: 3a131133)

Reply via email to