You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hbase.apache.org by ps...@apache.org on 2022/03/02 08:44:09 UTC

[hbase] 01/02: HBASE-26655. Implement new OAuth Bearer SASL plugin

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

psomogyi pushed a commit to branch HBASE-26553
in repository https://gitbox.apache.org/repos/asf/hbase.git

commit 783a05df870db2a639dacfe4d0bf5cedba25b0f6
Author: Andor Molnar <an...@cloudera.com>
AuthorDate: Thu Jan 20 15:31:30 2022 -0500

    HBASE-26655. Implement new OAuth Bearer SASL plugin
    
    Signed-off-by: Josh Elser <el...@apache.org>
---
 .../oauthbearer/OAuthBearerTokenCallback.java      | 122 +++++++++++
 .../internals/OAuthBearerSaslClient.java           | 193 +++++++++++++++++
 .../internals/OAuthBearerSaslClientProvider.java   |  38 ++++
 .../OAuthBearerSaslAuthenticationProvider.java     |  42 ++++
 ...AuthBearerSaslClientAuthenticationProvider.java | 155 ++++++++++++++
 .../provider/OAuthBearerSaslProviderSelector.java  |  69 ++++++
 .../hbase/security/token/OAuthBearerTokenUtil.java |  75 +++++++
 .../oauthbearer/OAuthBearerTokenCallbackTest.java  |  76 +++++++
 .../internals/OAuthBearerSaslClientTest.java       |  76 +++++++
 .../OAuthBearerSaslClientCallbackHandlerTest.java  | 108 ++++++++++
 .../exceptions/IllegalSaslStateException.java      |  35 +++
 .../exceptions/SaslAuthenticationException.java    |  47 ++++
 .../security/auth/AuthenticateCallbackHandler.java |  48 +++++
 .../security/oauthbearer/OAuthBearerToken.java     |  75 +++++++
 .../security/oauthbearer/OAuthBearerUtils.java     |  66 ++++++
 .../OAuthBearerClientInitialResponse.java          | 146 +++++++++++++
 .../hbase/security/oauthbearer/JwtTestUtils.java   | 117 ++++++++++
 .../OAuthBearerClientInitialResponseTest.java      | 110 ++++++++++
 .../hadoop/hbase/util/ClassLoaderTestHelper.java   |   2 -
 .../hbase/jwt/client/example/JwtClientExample.java | 111 ++++++++++
 .../src/main/resources/META-INF/LICENSE.vm         |   2 +-
 hbase-server/pom.xml                               |   4 +
 .../hadoop/hbase/ipc/ServerRpcConnection.java      |   2 +-
 .../oauthbearer/OAuthBearerValidatorCallback.java  | 156 ++++++++++++++
 .../internals/OAuthBearerSaslServer.java           | 238 +++++++++++++++++++++
 .../internals/OAuthBearerSaslServerProvider.java   |  38 ++++
 .../internals/knox/OAuthBearerConfigException.java |  37 ++++
 .../knox/OAuthBearerIllegalTokenException.java     |  65 ++++++
 .../internals/knox/OAuthBearerSignedJwt.java       | 193 +++++++++++++++++
 ...uthBearerSignedJwtValidatorCallbackHandler.java | 196 +++++++++++++++++
 .../knox/OAuthBearerValidationResult.java          | 135 ++++++++++++
 ...AuthBearerSaslServerAuthenticationProvider.java | 100 +++++++++
 .../security/oauthbearer/OAuthBearerTokenMock.java |  34 +++
 .../OAuthBearerValidatorCallbackTest.java          |  75 +++++++
 .../internals/OAuthBearerSaslServerTest.java       | 119 +++++++++++
 .../internals/knox/OAuthBearerSignedJwtTest.java   | 121 +++++++++++
 ...earerSignedJwtValidatorCallbackHandlerTest.java | 166 ++++++++++++++
 pom.xml                                            |   7 +-
 38 files changed, 3394 insertions(+), 5 deletions(-)

diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java
new file mode 100644
index 0000000..a9e28ef
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java
@@ -0,0 +1,122 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslClient} and {@code Login}
+ * implementations when they require an OAuth 2 bearer token. Callback handlers
+ * should use the {@link #error(String, String, String)} method to communicate
+ * errors returned by the authorization server as per
+ * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+ * 2.0 Authorization Framework</a>. Callback handlers should communicate other
+ * problems by raising an {@code IOException}.
+ * <p>
+ * This class was introduced in 3.0.0 and, while it feels stable, it could
+ * evolve. We will try to evolve the API in a compatible manner, but we reserve
+ * the right to make breaking changes in minor releases, if necessary. We will
+ * update the {@code InterfaceStability} annotation and this notice once the API
+ * is considered stable.
+ */
+@InterfaceAudience.Private
+public class OAuthBearerTokenCallback implements Callback {
+  private OAuthBearerToken token = null;
+  private String errorCode = null;
+  private String errorDescription = null;
+  private String errorUri = null;
+
+  /**
+   * Return the (potentially null) token
+   *
+   * @return the (potentially null) token
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * Return the optional (but always non-empty if not null) error code as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the optional (but always non-empty if not null) error code
+   */
+  public String errorCode() {
+    return errorCode;
+  }
+
+  /**
+   * Return the (potentially null) error description as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the (potentially null) error description
+   */
+  public String errorDescription() {
+    return errorDescription;
+  }
+
+  /**
+   * Return the (potentially null) error URI as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the (potentially null) error URI
+   */
+  public String errorUri() {
+    return errorUri;
+  }
+
+  /**
+   * Set the token. All error-related values are cleared.
+   *
+   * @param token
+   *            the optional token to set
+   */
+  public void token(OAuthBearerToken token) {
+    this.token = token;
+    this.errorCode = null;
+    this.errorDescription = null;
+    this.errorUri = null;
+  }
+
+  /**
+   * Set the error values as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+   * 2.0 Authorization Framework</a>. Any token is cleared.
+   *
+   * @param errorCode
+   *            the mandatory error code to set
+   * @param errorDescription
+   *            the optional error description to set
+   * @param errorUri
+   *            the optional error URI to set
+   */
+  public void error(String errorCode, String errorDescription, String errorUri) {
+    if (StringUtils.isEmpty(errorCode)) {
+      throw new IllegalArgumentException("error code must not be empty");
+    }
+    this.errorCode = errorCode;
+    this.errorDescription = errorDescription;
+    this.errorUri = errorUri;
+    this.token = null;
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java
new file mode 100644
index 0000000..2fa07b5
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java
@@ -0,0 +1,193 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import javax.security.sasl.SaslClientFactory;
+import javax.security.sasl.SaslException;
+import org.apache.hadoop.hbase.exceptions.IllegalSaslStateException;
+import org.apache.hadoop.hbase.security.SaslUtil;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code SaslClient} implementation for SASL/OAUTHBEARER in Kafka. This
+ * implementation requires an instance of {@code AuthenticateCallbackHandler}
+ * that can handle an instance of {@link OAuthBearerTokenCallback} and return
+ * the {@link OAuthBearerToken} generated by the {@code login()} event on the
+ * {@code LoginContext}.
+ *
+ * See <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750 Section 2.1</a>
+ *
+ *   This class has been copy-and-pasted from Kafka codebase.
+ */
+@InterfaceAudience.Public
+public class OAuthBearerSaslClient implements SaslClient {
+  static final byte BYTE_CONTROL_A = (byte) 0x01;
+  private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslClient.class);
+  private final CallbackHandler callbackHandler;
+
+  enum State {
+    SEND_CLIENT_FIRST_MESSAGE, RECEIVE_SERVER_FIRST_MESSAGE, RECEIVE_SERVER_MESSAGE_AFTER_FAILURE,
+    COMPLETE, FAILED
+  }
+
+  private State state;
+
+  public OAuthBearerSaslClient(AuthenticateCallbackHandler callbackHandler) {
+    this.callbackHandler = Objects.requireNonNull(callbackHandler);
+    setState(State.SEND_CLIENT_FIRST_MESSAGE);
+  }
+
+  public CallbackHandler callbackHandler() {
+    return callbackHandler;
+  }
+
+  @Override
+  public String getMechanismName() {
+    return OAUTHBEARER_MECHANISM;
+  }
+
+  @Override
+  public boolean hasInitialResponse() {
+    return true;
+  }
+
+  @Override
+  public byte[] evaluateChallenge(byte[] challenge) throws SaslException {
+    try {
+      OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+      switch (state) {
+        case SEND_CLIENT_FIRST_MESSAGE:
+          if (challenge != null && challenge.length != 0) {
+            throw new SaslException("Expected empty challenge");
+          }
+          callbackHandler().handle(new Callback[] {callback});
+          setState(State.RECEIVE_SERVER_FIRST_MESSAGE);
+          return new OAuthBearerClientInitialResponse(callback.token().value()).toBytes();
+        case RECEIVE_SERVER_FIRST_MESSAGE:
+          if (challenge != null && challenge.length != 0) {
+            String jsonErrorResponse = new String(challenge, StandardCharsets.UTF_8);
+            if (LOG.isDebugEnabled()) {
+              LOG.debug("Sending %%x01 response to server after receiving an error: {}",
+                jsonErrorResponse);
+            }
+            setState(State.RECEIVE_SERVER_MESSAGE_AFTER_FAILURE);
+            return new byte[] {BYTE_CONTROL_A};
+          }
+          callbackHandler().handle(new Callback[] {callback});
+          if (LOG.isDebugEnabled()) {
+            LOG.debug("Successfully authenticated as {}", callback.token().principalName());
+          }
+          setState(State.COMPLETE);
+          return null;
+        default:
+          throw new IllegalSaslStateException("Unexpected challenge in Sasl client state " + state);
+      }
+    } catch (SaslException e) {
+      setState(State.FAILED);
+      throw e;
+    } catch (IOException | UnsupportedCallbackException e) {
+      setState(State.FAILED);
+      throw new SaslException(e.getMessage(), e);
+    }
+  }
+
+  @Override
+  public boolean isComplete() {
+    return state == State.COMPLETE;
+  }
+
+  @Override
+  public byte[] unwrap(byte[] incoming, int offset, int len) {
+    if (!isComplete()) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    return Arrays.copyOfRange(incoming, offset, offset + len);
+  }
+
+  @Override
+  public byte[] wrap(byte[] outgoing, int offset, int len) {
+    if (!isComplete()) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    return Arrays.copyOfRange(outgoing, offset, offset + len);
+  }
+
+  @Override
+  public Object getNegotiatedProperty(String propName) {
+    if (!isComplete()) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    if (Sasl.QOP.equals(propName)) {
+      return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop();
+    }
+    return null;
+  }
+
+  @Override
+  public void dispose() {
+  }
+
+  private void setState(State state) {
+    LOG.debug("Setting SASL/{} client state to {}", OAUTHBEARER_MECHANISM, state);
+    this.state = state;
+  }
+
+  public static class OAuthBearerSaslClientFactory implements SaslClientFactory {
+    @Override
+    public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol,
+      String serverName, Map<String, ?> props, CallbackHandler callbackHandler) {
+      String[] mechanismNamesCompatibleWithPolicy = getMechanismNames(props);
+      for (String mechanism : mechanisms) {
+        for (String s : mechanismNamesCompatibleWithPolicy) {
+          if (s.equals(mechanism)) {
+            if (!(Objects.requireNonNull(callbackHandler) instanceof AuthenticateCallbackHandler)) {
+              throw new IllegalArgumentException(
+                String.format("Callback handler must be castable to %s: %s",
+                  AuthenticateCallbackHandler.class.getName(),
+                  callbackHandler.getClass().getName()));
+            }
+            return new OAuthBearerSaslClient((AuthenticateCallbackHandler) callbackHandler);
+          }
+        }
+      }
+      return null;
+    }
+
+    @Override
+    public String[] getMechanismNames(Map<String, ?> props) {
+      return OAuthBearerUtils.mechanismNamesCompatibleWithPolicy(props);
+    }
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java
new file mode 100644
index 0000000..3ba7217
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java
@@ -0,0 +1,38 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import java.security.Provider;
+import java.security.Security;
+import org.apache.yetus.audience.InterfaceAudience;
+
+@InterfaceAudience.Public
+public class OAuthBearerSaslClientProvider extends Provider {
+  private static final long serialVersionUID = 1L;
+
+  protected OAuthBearerSaslClientProvider() {
+    super("SASL/OAUTHBEARER Client Provider", 1.0, "SASL/OAUTHBEARER Client Provider for HBase");
+    put("SaslClientFactory." + OAUTHBEARER_MECHANISM,
+      OAuthBearerSaslClient.OAuthBearerSaslClientFactory.class.getName());
+  }
+
+  public static void initialize() {
+    Security.addProvider(new OAuthBearerSaslClientProvider());
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java
new file mode 100644
index 0000000..8b4dcfe
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.hadoop.hbase.security.provider;
+
+import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Base client for client/server implementations for the OAuth Bearer (JWT) token auth'n method.
+ */
+@InterfaceAudience.Private
+public class OAuthBearerSaslAuthenticationProvider extends BuiltInSaslAuthenticationProvider {
+
+  public static final SaslAuthMethod SASL_AUTH_METHOD = new SaslAuthMethod(
+      "OAUTHBEARER", (byte)83, "OAUTHBEARER", UserGroupInformation.AuthenticationMethod.TOKEN);
+
+  @Override
+  public SaslAuthMethod getSaslAuthMethod() {
+    return SASL_AUTH_METHOD;
+  }
+
+  @Override
+  public String getTokenKind() {
+    return TOKEN_KIND;
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java
new file mode 100644
index 0000000..915706c
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java
@@ -0,0 +1,155 @@
+/*
+ * 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.hadoop.hbase.security.provider;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.security.AccessController;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.security.SaslUtil;
+import org.apache.hadoop.hbase.security.SecurityInfo;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback;
+import org.apache.hadoop.security.token.Token;
+import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.hadoop.hbase.shaded.protobuf.generated.RPCProtos;
+
+@InterfaceAudience.Private
+public class OAuthBearerSaslClientAuthenticationProvider
+    extends OAuthBearerSaslAuthenticationProvider
+    implements SaslClientAuthenticationProvider {
+
+  @Override
+  public SaslClient createClient(Configuration conf, InetAddress serverAddr,
+                                 SecurityInfo securityInfo, Token<? extends TokenIdentifier> token,
+                                 boolean fallbackAllowed,
+                                 Map<String, String> saslProps) throws IOException {
+    AuthenticateCallbackHandler callbackHandler = new OAuthBearerSaslClientCallbackHandler();
+    callbackHandler.configure(conf, getSaslAuthMethod().getSaslMechanism(), saslProps);
+    return Sasl.createSaslClient(new String[] { getSaslAuthMethod().getSaslMechanism() }, null,
+        null, SaslUtil.SASL_DEFAULT_REALM, saslProps, callbackHandler);
+  }
+
+  public static class OAuthBearerSaslClientCallbackHandler implements AuthenticateCallbackHandler {
+    private static final Logger LOG =
+      LoggerFactory.getLogger(OAuthBearerSaslClientCallbackHandler.class);
+    private boolean configured = false;
+
+    @Override public void configure(Configuration configs, String saslMechanism,
+      Map<String, String> saslProps) {
+      if (!OAUTHBEARER_MECHANISM.equals(saslMechanism)) {
+        throw new IllegalArgumentException(
+          String.format("Unexpected SASL mechanism: %s", saslMechanism));
+      }
+      this.configured = true;
+    }
+
+    @Override
+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+      if (!configured) {
+        throw new IllegalStateException(
+          "OAuthBearerSaslClientCallbackHandler handler must be configured first.");
+      }
+
+      for (Callback callback : callbacks) {
+        if (callback instanceof OAuthBearerTokenCallback) {
+          handleCallback((OAuthBearerTokenCallback) callback);
+        } else {
+          throw new UnsupportedCallbackException(callback);
+        }
+      }
+    }
+
+    private void handleCallback(OAuthBearerTokenCallback callback) throws IOException {
+      if (callback.token() != null) {
+        throw new IllegalArgumentException("Callback had a token already");
+      }
+      Subject subject = Subject.getSubject(AccessController.getContext());
+      Set<OAuthBearerToken> privateCredentials = subject != null
+        ? subject.getPrivateCredentials(OAuthBearerToken.class)
+        : Collections.emptySet();
+      callback.token(choosePrivateCredential(privateCredentials));
+    }
+
+    private OAuthBearerToken choosePrivateCredential(Set<OAuthBearerToken> privateCredentials)
+      throws IOException {
+      if (privateCredentials.size() == 0) {
+        throw new IOException("No OAuth Bearer tokens in Subject's private credentials");
+      }
+      if (privateCredentials.size() == 1) {
+        LOG.debug("Found 1 OAuthBearer token");
+        return privateCredentials.iterator().next();
+      } else {
+        /*
+         * There a very small window of time upon token refresh (on the order of milliseconds)
+         * where both an old and a new token appear on the Subject's private credentials.
+         * Rather than implement a lock to eliminate this window, we will deal with it by
+         * checking for the existence of multiple tokens and choosing the one that has the
+         * longest lifetime.  It is also possible that a bug could cause multiple tokens to
+         * exist (e.g. KAFKA-7902), so dealing with the unlikely possibility that occurs
+         * during normal operation also allows us to deal more robustly with potential bugs.
+         */
+        NavigableSet<OAuthBearerToken> sortedByLifetime =
+          new TreeSet<>(
+            new Comparator<OAuthBearerToken>() {
+              @Override
+              public int compare(OAuthBearerToken o1, OAuthBearerToken o2) {
+                return Long.compare(o1.lifetimeMs(), o2.lifetimeMs());
+              }
+            });
+        sortedByLifetime.addAll(privateCredentials);
+        if (LOG.isWarnEnabled()) {
+          LOG.warn("Found {} OAuth Bearer tokens in Subject's private credentials; " +
+              "the oldest expires at {}, will use the newest, which expires at {}",
+            sortedByLifetime.size(),
+            LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.first().lifetimeMs()),
+              ZoneId.systemDefault()),
+            LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.last().lifetimeMs()),
+              ZoneId.systemDefault()));
+        }
+        return sortedByLifetime.last();
+      }
+    }
+  }
+
+  @Override
+  public RPCProtos.UserInformation getUserInfo(User user) {
+    // Don't send user for token auth. Copied from RpcConnection.
+    return null;
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java
new file mode 100644
index 0000000..88c2eed
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hadoop.hbase.security.provider;
+
+import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND;
+import java.util.Collection;
+import java.util.Optional;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.util.Pair;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.security.token.Token;
+import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@InterfaceAudience.Private
+public class OAuthBearerSaslProviderSelector extends BuiltInProviderSelector {
+
+  private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslProviderSelector.class);
+
+  private final Text OAUTHBEARER_TOKEN_KIND_TEXT =
+    new Text(TOKEN_KIND);
+  private OAuthBearerSaslClientAuthenticationProvider oauthbearer;
+
+  @Override public void configure(Configuration conf,
+    Collection<SaslClientAuthenticationProvider> providers) {
+    super.configure(conf, providers);
+
+    this.oauthbearer = (OAuthBearerSaslClientAuthenticationProvider) providers.stream()
+      .filter((p) -> p instanceof OAuthBearerSaslClientAuthenticationProvider)
+      .findFirst()
+      .orElseThrow(() -> new RuntimeException(
+        "OAuthBearerSaslClientAuthenticationProvider not loaded"));
+  }
+
+  @Override
+  public Pair<SaslClientAuthenticationProvider, Token<? extends TokenIdentifier>> selectProvider(
+    String clusterId, User user) {
+    Pair<SaslClientAuthenticationProvider, Token<? extends TokenIdentifier>> pair =
+      super.selectProvider(clusterId, user);
+
+    Optional<Token<?>> optional = user.getTokens().stream()
+      .filter((t) -> OAUTHBEARER_TOKEN_KIND_TEXT.equals(t.getKind()))
+      .findFirst();
+    if (optional.isPresent()) {
+      LOG.info("OAuthBearer token found in user tokens");
+      return new Pair<>(oauthbearer, optional.get());
+    }
+
+    return pair;
+  }
+}
diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java
new file mode 100644
index 0000000..d9e42d5
--- /dev/null
+++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java
@@ -0,0 +1,75 @@
+/**
+ *
+ * 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.hadoop.hbase.security.token;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import javax.security.auth.Subject;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslClientProvider;
+import org.apache.hadoop.io.Text;
+import org.apache.hadoop.security.token.Token;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility methods for obtaining OAuthBearer / JWT authentication tokens.
+ */
+@InterfaceAudience.Public
+public final class OAuthBearerTokenUtil {
+  private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerTokenUtil.class);
+  public static final String TOKEN_KIND = "JWT_AUTH_TOKEN";
+
+  static {
+    OAuthBearerSaslClientProvider.initialize(); // not part of public API
+    LOG.info("OAuthBearer SASL client provider has been initialized");
+  }
+
+  private OAuthBearerTokenUtil() {  }
+
+  /**
+   * Add token to user's subject private credentials and a hint to provider selector
+   * to correctly select OAuthBearer SASL provider.
+   */
+  public static void addTokenForUser(User user, String encodedToken, long lifetimeMs) {
+    user.addToken(new Token<>(null, null, new Text(TOKEN_KIND), null));
+    user.runAs(new PrivilegedAction<Object>() {
+      @Override public Object run() {
+        Subject subject = Subject.getSubject(AccessController.getContext());
+        OAuthBearerToken jwt = new OAuthBearerToken() {
+          @Override public String value() {
+            return encodedToken;
+          }
+
+          @Override public long lifetimeMs() {
+            return lifetimeMs;
+          }
+
+          @Override public String principalName() {
+            return user.getName();
+          }
+        };
+        subject.getPrivateCredentials().add(jwt);
+        return null;
+      }
+    });
+  }
+}
diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java
new file mode 100644
index 0000000..06ce657
--- /dev/null
+++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerTokenCallbackTest {
+
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerTokenCallbackTest.class);
+
+  private static final OAuthBearerToken TOKEN = new OAuthBearerToken() {
+    @Override
+    public String value() {
+      return "value";
+    }
+
+    @Override
+    public String principalName() {
+      return "principalName";
+    }
+
+    @Override
+    public long lifetimeMs() {
+      return 0;
+    }
+  };
+
+  @Test
+  public void testError() {
+    String errorCode = "errorCode";
+    String errorDescription = "errorDescription";
+    String errorUri = "errorUri";
+    OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+    callback.error(errorCode, errorDescription, errorUri);
+    assertEquals(errorCode, callback.errorCode());
+    assertEquals(errorDescription, callback.errorDescription());
+    assertEquals(errorUri, callback.errorUri());
+    assertNull(callback.token());
+  }
+
+  @Test
+  public void testToken() {
+    OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+    callback.token(TOKEN);
+    assertSame(TOKEN, callback.token());
+    assertNull(callback.errorCode());
+    assertNull(callback.errorDescription());
+    assertNull(callback.errorUri());
+  }
+}
diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java
new file mode 100644
index 0000000..0267084
--- /dev/null
+++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.junit.Assert.assertEquals;
+import java.nio.charset.StandardCharsets;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerSaslClientTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerSaslClientTest.class);
+
+  public static class ExtensionsCallbackHandler implements AuthenticateCallbackHandler {
+
+    @Override
+    public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
+      for (Callback callback : callbacks) {
+        if (callback instanceof OAuthBearerTokenCallback) {
+          ((OAuthBearerTokenCallback) callback).token(new OAuthBearerToken() {
+            @Override public String value() {
+              return "";
+            }
+
+            @Override public long lifetimeMs() {
+              return 100;
+            }
+
+            @Override public String principalName() {
+              return "principalName";
+            }
+          });
+        } else {
+          throw new UnsupportedCallbackException(callback);
+        }
+      }
+    }
+  }
+
+  @Test
+  public void testAttachesExtensionsToFirstClientMessage() throws Exception {
+    String expectedToken = new String(
+      new OAuthBearerClientInitialResponse("").toBytes(), StandardCharsets.UTF_8);
+    OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler());
+    String message = new String(client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)),
+      StandardCharsets.UTF_8);
+    assertEquals(expectedToken, message);
+  }
+
+}
diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java
new file mode 100644
index 0000000..caf85db
--- /dev/null
+++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.hadoop.hbase.security.provider;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collections;
+import java.util.Set;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerSaslClientCallbackHandlerTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerSaslClientCallbackHandlerTest.class);
+
+  private static OAuthBearerToken createTokenWithLifetimeMillis(final long lifetimeMillis) {
+    return new OAuthBearerToken() {
+      @Override
+      public String value() {
+        return null;
+      }
+
+      @Override
+      public String principalName() {
+        return null;
+      }
+
+      @Override
+      public long lifetimeMs() {
+        return lifetimeMillis;
+      }
+    };
+  }
+
+  @Test
+  public void testWithZeroTokens() {
+    OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler =
+      createCallbackHandler();
+    PrivilegedActionException e =
+      assertThrows(PrivilegedActionException.class, () -> Subject.doAs(new Subject(),
+      (PrivilegedExceptionAction<Void>) () -> {
+          OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+          handler.handle(new Callback[] {callback});
+          return null;
+        }
+    ));
+    assertEquals(IOException.class, e.getCause().getClass());
+  }
+
+  @Test
+  public void testWithPotentiallyMultipleTokens() throws Exception {
+    OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler =
+      createCallbackHandler();
+    Subject.doAs(new Subject(), (PrivilegedExceptionAction<Void>) () -> {
+      final int maxTokens = 4;
+      final Set<Object> privateCredentials = Subject.getSubject(AccessController.getContext())
+        .getPrivateCredentials();
+      privateCredentials.clear();
+      for (int num = 1; num <= maxTokens; ++num) {
+        privateCredentials.add(createTokenWithLifetimeMillis(num));
+        privateCredentials.add(createTokenWithLifetimeMillis(-num));
+        OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+        handler.handle(new Callback[] {callback});
+        assertEquals(num, callback.token().lifetimeMs());
+      }
+      return null;
+    });
+  }
+
+  private static OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler
+    createCallbackHandler() {
+    OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler =
+      new OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler();
+    handler.configure(new Configuration(), OAUTHBEARER_MECHANISM, Collections.emptyMap());
+    return handler;
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java
new file mode 100644
index 0000000..ce7d1f7
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.hadoop.hbase.exceptions;
+
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * This exception indicates unexpected requests prior to SASL authentication.
+ * This could be due to misconfigured security.
+ */
+@InterfaceAudience.Public
+public class IllegalSaslStateException extends IllegalStateException {
+
+  private static final long serialVersionUID = 1L;
+
+  public IllegalSaslStateException(String message) {
+    super(message);
+  }
+
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java
new file mode 100644
index 0000000..3f4866e
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.hadoop.hbase.exceptions;
+
+import javax.security.sasl.SaslServer;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * This exception indicates that SASL authentication has failed. The error message
+ * in the exception indicates the actual cause of failure.
+ * <p>
+ * SASL authentication failures typically indicate invalid credentials, but
+ * could also include other failures specific to the SASL mechanism used
+ * for authentication.
+ * </p>
+ * <p><b>Note:</b>If {@link SaslServer#evaluateResponse(byte[])} throws this exception during
+ * authentication, the message from the exception will be sent to clients in the SaslAuthenticate
+ * response. Custom {@link SaslServer} implementations may throw this exception in order to
+ * provide custom error messages to clients, but should take care not to include any
+ * security-critical information in the message that should not be leaked to unauthenticated
+ * clients.
+ * </p>
+ */
+@InterfaceAudience.Public
+public class SaslAuthenticationException extends RuntimeException {
+
+  private static final long serialVersionUID = 1L;
+
+  public SaslAuthenticationException(String message) {
+    super(message);
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java
new file mode 100644
index 0000000..1329e9a
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java
@@ -0,0 +1,48 @@
+/*
+ * 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.hadoop.hbase.security.auth;
+
+import java.util.Map;
+import javax.security.auth.callback.CallbackHandler;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/*
+ * Callback handler for SASL-based authentication
+ */
+@InterfaceAudience.Private
+public interface AuthenticateCallbackHandler extends CallbackHandler {
+
+  /**
+   * Configures this callback handler for the specified SASL mechanism.
+   *
+   * @param configs Key-value pairs containing the parsed configuration options of
+   *        the client or server. Note that these are the HBase configuration options
+   *        and not the JAAS configuration options. JAAS config options may be obtained
+   *        from `jaasConfigEntries` for callbacks which obtain some configs from the
+   *        JAAS configuration. For configs that may be specified as both HBase config
+   *        as well as JAAS config (e.g. sasl.kerberos.service.name), the configuration
+   *        is treated as invalid if conflicting values are provided.
+   * @param saslMechanism Negotiated SASL mechanism. For clients, this is the SASL
+   *        mechanism configured for the client. For brokers, this is the mechanism
+   *        negotiated with the client and is one of the mechanisms enabled on the broker.
+   * @param saslProps SASL properties provided by the SASL library.
+   */
+  default void configure(
+    Configuration configs, String saslMechanism, Map<String, String> saslProps) {}
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java
new file mode 100644
index 0000000..769bcee
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java
@@ -0,0 +1,75 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.yetus.audience.InterfaceStability;
+
+/**
+ * The <code>b64token</code> value as defined in
+ * <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750 Section
+ * 2.1</a> along with the token's specific scope and lifetime and principal
+ * name.
+ * <p>
+ * A network request would be required to re-hydrate an opaque token, and that
+ * could result in (for example) an {@code IOException}, but retrievers for
+ * various attributes ({@link #lifetimeMs()}, etc.) declare no
+ * exceptions. Therefore, if a network request is required for any of these
+ * retriever methods, that request could be performed at construction time so
+ * that the various attributes can be reliably provided thereafter. For example,
+ * a constructor might declare {@code throws IOException} in such a case.
+ * Alternatively, the retrievers could throw unchecked exceptions.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc6749#section-1.4">RFC 6749
+ *      Section 1.4</a> and
+ *      <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750
+ *      Section 2.1</a>
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+public interface OAuthBearerToken {
+  /**
+   * The <code>b64token</code> value as defined in
+   * <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750 Section
+   * 2.1</a>
+   *
+   * @return <code>b64token</code> value as defined in
+   *         <a href="https://tools.ietf.org/html/rfc6750#section-2.1">RFC 6750
+   *         Section 2.1</a>
+   */
+  String value();
+
+  /**
+   * The token's lifetime, expressed as the number of milliseconds since the
+   * epoch, as per <a href="https://tools.ietf.org/html/rfc6749#section-1.4">RFC
+   * 6749 Section 1.4</a>
+   *
+   * @return the token'slifetime, expressed as the number of milliseconds since
+   *         the epoch, as per
+   *         <a href="https://tools.ietf.org/html/rfc6749#section-1.4">RFC 6749
+   *         Section 1.4</a>.
+   */
+  long lifetimeMs();
+
+  /**
+   * The name of the principal to which this credential applies
+   *
+   * @return the always non-null/non-empty principal name
+   */
+  String principalName();
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java
new file mode 100644
index 0000000..19796e8
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.sasl.Sasl;
+import org.apache.yetus.audience.InterfaceAudience;
+
+@InterfaceAudience.Private
+public final class OAuthBearerUtils {
+  public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER";
+
+  /**
+   * Verifies configuration for OAuth Bearer authentication mechanism.
+   * Throws RuntimeException if PlainText is not allowed.
+   */
+  public static String[] mechanismNamesCompatibleWithPolicy(Map<String, ?> props) {
+    if (props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT)))) {
+      throw new RuntimeException("OAuth Bearer authentication mech cannot be used if plaintext is "
+        + "disallowed.");
+    }
+    return new String[] { OAUTHBEARER_MECHANISM };
+  }
+
+  /**
+   *  Converts an extensions string into a {@code Map<String, String>}.
+   *
+   *  Example:
+   *      {@code parseMap("key=hey,keyTwo=hi,keyThree=hello", "=", ",") =>
+   *      { key: "hey", keyTwo: "hi", keyThree: "hello" }}
+   *
+   */
+  public static Map<String, String> parseMap(String mapStr,
+    String keyValueSeparator, String elementSeparator) {
+    Map<String, String> map = new HashMap<>();
+
+    if (!mapStr.isEmpty()) {
+      String[] attrvals = mapStr.split(elementSeparator);
+      for (String attrval : attrvals) {
+        String[] array = attrval.split(keyValueSeparator, 2);
+        map.put(array[0], array[1]);
+      }
+    }
+    return map;
+  }
+
+  private OAuthBearerUtils() {
+    // empty
+  }
+}
diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java
new file mode 100644
index 0000000..2bfd66a
--- /dev/null
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java
@@ -0,0 +1,146 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.security.sasl.SaslException;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * OAuthBearer SASL client's initial message to the server.
+ *
+ * This class has been copy-and-pasted from Kafka codebase.
+ */
+@InterfaceAudience.Public
+public class OAuthBearerClientInitialResponse {
+  private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerClientInitialResponse.class);
+  static final String SEPARATOR = "\u0001";
+
+  private static final String SASLNAME = "(?:[\\x01-\\x7F&&[^=,]]|=2C|=3D)+";
+  private static final String KEY = "[A-Za-z]+";
+  private static final String VALUE = "[\\x21-\\x7E \t\r\n]+";
+
+  private static final String KVPAIRS = String.format("(%s=%s%s)*", KEY, VALUE, SEPARATOR);
+  private static final Pattern AUTH_PATTERN =
+    Pattern.compile("(?<scheme>[\\w]+)[ ]+(?<token>[-_\\.a-zA-Z0-9]+)");
+  private static final Pattern CLIENT_INITIAL_RESPONSE_PATTERN = Pattern.compile(
+    String.format("n,(a=(?<authzid>%s))?,%s(?<kvpairs>%s)%s",
+      SASLNAME, SEPARATOR, KVPAIRS, SEPARATOR));
+  public static final String AUTH_KEY = "auth";
+
+  private final String tokenValue;
+  private final String authorizationId;
+
+  public OAuthBearerClientInitialResponse(byte[] response) throws SaslException {
+    LOG.trace("Client initial response parsing started");
+    String responseMsg = new String(response, StandardCharsets.UTF_8);
+    Matcher matcher = CLIENT_INITIAL_RESPONSE_PATTERN.matcher(responseMsg);
+    if (!matcher.matches()) {
+      throw new SaslException("Invalid OAUTHBEARER client first message");
+    }
+    LOG.trace("Client initial response matches pattern");
+    String authzid = matcher.group("authzid");
+    this.authorizationId = authzid == null ? "" : authzid;
+    String kvPairs = matcher.group("kvpairs");
+    Map<String, String> properties = OAuthBearerUtils.parseMap(kvPairs, "=", SEPARATOR);
+    String auth = properties.get(AUTH_KEY);
+    if (auth == null) {
+      throw new SaslException("Invalid OAUTHBEARER client first message: 'auth' not specified");
+    }
+    LOG.trace("Auth key found in client initial response");
+    properties.remove(AUTH_KEY);
+    Matcher authMatcher = AUTH_PATTERN.matcher(auth);
+    if (!authMatcher.matches()) {
+      throw new SaslException("Invalid OAUTHBEARER client first message: invalid 'auth' format");
+    }
+    LOG.trace("Client initial response auth matches pattern");
+    if (!"bearer".equalsIgnoreCase(authMatcher.group("scheme"))) {
+      String msg = String.format("Invalid scheme in OAUTHBEARER client first message: %s",
+        matcher.group("scheme"));
+      throw new SaslException(msg);
+    }
+    this.tokenValue = authMatcher.group("token");
+    LOG.trace("Client initial response parsing finished");
+  }
+
+  /**
+   * Constructor
+   *
+   * @param tokenValue
+   *            the mandatory token value
+   * @throws SaslException
+   *             if any extension name or value fails to conform to the required
+   *             regular expression as defined by the specification, or if the
+   *             reserved {@code auth} appears as a key
+   */
+  public OAuthBearerClientInitialResponse(String tokenValue) {
+    this(tokenValue, "");
+  }
+
+  /**
+   * Constructor
+   *
+   * @param tokenValue
+   *            the mandatory token value
+   * @param authorizationId
+   *            the optional authorization ID
+   * @throws SaslException
+   *             if any extension name or value fails to conform to the required
+   *             regular expression as defined by the specification, or if the
+   *             reserved {@code auth} appears as a key
+   */
+  public OAuthBearerClientInitialResponse(String tokenValue, String authorizationId) {
+    this.tokenValue = Objects.requireNonNull(tokenValue, "token value must not be null");
+    this.authorizationId = authorizationId == null ? "" : authorizationId;
+  }
+
+  public byte[] toBytes() {
+    String authzid = authorizationId.isEmpty() ? "" : "a=" + authorizationId;
+
+    String message = String.format("n,%s,%sauth=Bearer %s%s%s", authzid,
+      SEPARATOR, tokenValue, SEPARATOR, SEPARATOR);
+
+    return Bytes.toBytes(message);
+  }
+
+  /**
+   * Return the always non-null token value
+   *
+   * @return the always non-null toklen value
+   */
+  public String tokenValue() {
+    return tokenValue;
+  }
+
+  /**
+   * Return the always non-null authorization ID
+   *
+   * @return the always non-null authorization ID
+   */
+  public String authorizationId() {
+    return authorizationId;
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java
new file mode 100644
index 0000000..39b4330
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java
@@ -0,0 +1,117 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import org.apache.yetus.audience.InterfaceAudience;
+
+@InterfaceAudience.Public
+public final class JwtTestUtils {
+  private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
+  public static final String USER = "user";
+
+  public static RSAKey generateRSAKey() throws JOSEException {
+    RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048);
+    return rsaKeyGenerator.keyID("1").generate();
+  }
+
+  public static String createSignedJwt(RSAKey rsaKey, String issuer, String subject,
+    LocalDate expirationTime, LocalDate issueTime, String audience)
+    throws JOSEException {
+    JWSHeader jwsHeader =
+      new JWSHeader.Builder(JWSAlgorithm.RS256)
+        .type(JOSEObjectType.JWT)
+        .keyID(rsaKey.getKeyID())
+        .build();
+    JWTClaimsSet payload = new JWTClaimsSet.Builder()
+      .issuer(issuer)
+      .subject(subject)
+      .issueTime(java.sql.Date.valueOf(issueTime))
+      .expirationTime(java.sql.Date.valueOf(expirationTime))
+      .audience(audience)
+      .build();
+    SignedJWT signedJwt = new SignedJWT(jwsHeader, payload);
+    signedJwt.sign(new RSASSASigner(rsaKey));
+    return signedJwt.serialize();
+  }
+
+  public static String createSignedJwt(RSAKey rsaKey) throws JOSEException {
+    LocalDateTime now = LocalDateTime.now(ZONE_ID);
+    JWSHeader jwsHeader =
+      new JWSHeader.Builder(JWSAlgorithm.RS256)
+        .type(JOSEObjectType.JWT)
+        .keyID(rsaKey.getKeyID())
+        .build();
+    JWTClaimsSet payload = new JWTClaimsSet.Builder()
+      .subject(USER)
+      .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1)))
+      .build();
+    SignedJWT signedJwt = new SignedJWT(jwsHeader, payload);
+    signedJwt.sign(new RSASSASigner(rsaKey));
+    return signedJwt.serialize();
+  }
+
+  public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) throws JOSEException {
+    LocalDateTime now = LocalDateTime.now(ZONE_ID);
+    JWSHeader jwsHeader =
+      new JWSHeader.Builder(JWSAlgorithm.RS256)
+        .type(JOSEObjectType.JWT)
+        .keyID(rsaKey.getKeyID())
+        .build();
+    JWTClaimsSet payload = new JWTClaimsSet.Builder()
+      .subject(USER)
+      .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1)))
+      .audience(aud)
+      .build();
+    SignedJWT signedJwt = new SignedJWT(jwsHeader, payload);
+    signedJwt.sign(new RSASSASigner(rsaKey));
+    return signedJwt.serialize();
+  }
+
+  public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws JOSEException {
+    LocalDateTime now = LocalDateTime.now(ZONE_ID);
+    JWSHeader jwsHeader =
+      new JWSHeader.Builder(JWSAlgorithm.RS256)
+        .type(JOSEObjectType.JWT)
+        .keyID(rsaKey.getKeyID())
+        .build();
+    JWTClaimsSet payload = new JWTClaimsSet.Builder()
+      .subject(USER)
+      .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1)))
+      .issuer(iss)
+      .build();
+    SignedJWT signedJwt = new SignedJWT(jwsHeader, payload);
+    signedJwt.sign(new RSASSASigner(rsaKey));
+    return signedJwt.serialize();
+  }
+
+  private JwtTestUtils() {
+    // empty
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java
new file mode 100644
index 0000000..86e7d46
--- /dev/null
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.junit.Assert.assertEquals;
+import java.nio.charset.StandardCharsets;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerClientInitialResponseTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerClientInitialResponseTest.class);
+
+  /*
+      Test how a client would build a response
+   */
+  @Test
+  public void testBuildClientResponseToBytes() {
+    String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001";
+
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse("123.345.567");
+
+    String message = new String(response.toBytes(), StandardCharsets.UTF_8);
+
+    assertEquals(expectedMesssage, message);
+  }
+
+  @Test
+  public void testBuildServerResponseToBytes() throws Exception {
+    String serverMessage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(serverMessage.getBytes(StandardCharsets.UTF_8));
+
+    String message = new String(response.toBytes(), StandardCharsets.UTF_8);
+
+    assertEquals(serverMessage, message);
+  }
+
+  @Test
+  public void testToken() throws Exception {
+    String message = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8));
+    assertEquals("123.345.567", response.tokenValue());
+    assertEquals("", response.authorizationId());
+  }
+
+  @Test
+  public void testAuthorizationId() throws Exception {
+    String message = "n,a=myuser,\u0001auth=Bearer 345\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8));
+    assertEquals("345", response.tokenValue());
+    assertEquals("myuser", response.authorizationId());
+  }
+
+  @Test
+  public void testExtensions() throws Exception {
+    String message =
+      "n,,\u0001propA=valueA1, valueA2\u0001auth=Bearer 567\u0001propB=valueB\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8));
+    assertEquals("567", response.tokenValue());
+    assertEquals("", response.authorizationId());
+  }
+
+  // The example in the RFC uses `vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==` as the token
+  // But since we use Base64Url encoding, padding is omitted. Hence this test verifies without '='.
+  @Test
+  public void testRfc7688Example() throws Exception {
+    String message = "n,a=user@example.com,\u0001host=server.example.com\u0001port=143\u0001" +
+      "auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8));
+    assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue());
+    assertEquals("user@example.com", response.authorizationId());
+  }
+
+  @Test
+  public void testNoExtensionsFromByteArray() throws Exception {
+    String message = "n,a=user@example.com,\u0001" +
+      "auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg\u0001\u0001";
+    OAuthBearerClientInitialResponse response =
+      new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8));
+    assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue());
+    assertEquals("user@example.com", response.authorizationId());
+  }
+}
diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java
index decc0da..094ffe6 100644
--- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java
+++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java
@@ -19,7 +19,6 @@ package org.apache.hadoop.hbase.util;
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileInputStream;
@@ -35,7 +34,6 @@ import javax.tools.JavaCompiler;
 import javax.tools.JavaFileObject;
 import javax.tools.StandardJavaFileManager;
 import javax.tools.ToolProvider;
-
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.Path;
 import org.slf4j.Logger;
diff --git a/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java
new file mode 100644
index 0000000..65eafc8
--- /dev/null
+++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java
@@ -0,0 +1,111 @@
+/**
+ * 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.hadoop.hbase.jwt.client.example;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ThreadLocalRandom;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.conf.Configured;
+import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.CellBuilderFactory;
+import org.apache.hadoop.hbase.CellBuilderType;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.client.Admin;
+import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
+import org.apache.hadoop.hbase.client.Connection;
+import org.apache.hadoop.hbase.client.ConnectionFactory;
+import org.apache.hadoop.hbase.client.Put;
+import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.client.TableDescriptor;
+import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.security.User;
+import org.apache.hadoop.hbase.security.UserProvider;
+import org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil;
+import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.util.Tool;
+import org.apache.hadoop.util.ToolRunner;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An example of using OAuthBearer (JWT) authentication with HBase RPC client.
+ */
+@InterfaceAudience.Private
+public class JwtClientExample extends Configured implements Tool {
+  private static final Logger LOG = LoggerFactory.getLogger(JwtClientExample.class);
+  private static final String JWT_TOKEN = "<base64_encoded_jwt_token>";
+
+  private static final byte[] FAMILY = Bytes.toBytes("d");
+
+  public JwtClientExample() {
+    Configuration conf = HBaseConfiguration.create();
+    conf.set("hbase.client.sasl.provider.class",
+      "org.apache.hadoop.hbase.security.provider.OAuthBearerSaslProviderSelector");
+    conf.set("hbase.client.sasl.provider.extras",
+      "org.apache.hadoop.hbase.security.provider.OAuthBearerSaslClientAuthenticationProvider");
+    setConf(conf);
+  }
+
+  @Override public int run(String[] args) throws Exception {
+    LOG.info("JWT client example has been started");
+
+    Configuration conf = getConf();
+    LOG.info("Config = " + conf.get("hbase.client.sasl.provider.class"));
+    UserProvider provider = UserProvider.instantiate(conf);
+    User user = provider.getCurrent();
+
+    OAuthBearerTokenUtil.addTokenForUser(user, JWT_TOKEN, 0);
+    LOG.info("JWT token added");
+
+    try (final Connection conn = ConnectionFactory.createConnection(conf, user)) {
+      LOG.info("Connected to HBase");
+      Admin admin = conn.getAdmin();
+
+      TableName tn = TableName.valueOf("jwt-test-table");
+      if (!admin.isTableAvailable(tn)) {
+        TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(tn)
+          .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(FAMILY).build())
+          .build();
+        admin.createTable(tableDescriptor);
+      }
+
+      Table table = conn.getTable(tn);
+      byte[] rk = Bytes.toBytes(ThreadLocalRandom.current().nextLong());
+      Put p = new Put(rk);
+      p.add(CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY)
+          .setRow(rk)
+          .setFamily(FAMILY)
+          .setType(Cell.Type.Put)
+          .setValue("test".getBytes(StandardCharsets.UTF_8))
+        .build());
+      table.put(p);
+
+      admin.disableTable(tn);
+      admin.deleteTable(tn);
+    }
+
+    LOG.info("JWT client example is done");
+    return 0;
+  }
+
+  public static void main(String[] args) throws Exception {
+    ToolRunner.run(new JwtClientExample(), args);
+  }
+}
diff --git a/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm b/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm
index ac7c4a7..fa48fe0 100644
--- a/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm
+++ b/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm
@@ -1343,7 +1343,7 @@ You can redistribute it and/or modify it under either the terms of the
 ## See this FAQ link for justifications: https://www.apache.org/legal/resolved.html
 ##
 ## NB: This list is later compared as lower-case. New entries must also be all lower-case
-#set($non_aggregate_fine = [ 'public domain', 'new bsd license', 'bsd license', 'bsd', 'bsd 2-clause license', 'mozilla public license version 1.1', 'mozilla public license version 2.0', 'creative commons attribution license, version 2.5', 'apache-2.0' ])
+#set($non_aggregate_fine = [ 'public domain', 'new bsd license', 'bsd license', 'bsd', 'bsd 2-clause license', 'bsd-3-clause', 'mozilla public license version 1.1', 'mozilla public license version 2.0', 'creative commons attribution license, version 2.5', 'apache-2.0' ])
 ## include LICENSE sections for anything not under ASL2.0
 #foreach( ${dep} in ${projects} )
 ## if there are no licenses we'll fail the build later, so
diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml
index 619ae14..c6d8870 100644
--- a/hbase-server/pom.xml
+++ b/hbase-server/pom.xml
@@ -534,6 +534,10 @@
       <artifactId>log4j-1.2-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>
+    </dependency>
   </dependencies>
   <profiles>
     <!-- Needs to make the profile in apache parent pom -->
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java
index 4ebc9fa..bddd3c5 100644
--- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java
@@ -365,7 +365,7 @@ abstract class ServerRpcConnection implements Closeable {
             throw e;
           }
           RpcServer.LOG.debug("Created SASL server with mechanism={}",
-              provider.getSaslAuthMethod().getAuthMethod());
+              provider.getSaslAuthMethod().getSaslMechanism());
         }
         RpcServer.LOG.debug("Read input token of size={} for processing by saslServer." +
             "evaluateResponse()", saslToken.limit());
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java
new file mode 100644
index 0000000..ec4b3c2
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java
@@ -0,0 +1,156 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to provide an OAuth 2 bearer token compact serialization for
+ * validation. Callback handlers should use the
+ * {@link #error(String, String, String)} method to communicate errors back to
+ * the SASL Client as per
+ * <a href="https://tools.ietf.org/html/rfc6749#section-5.2">RFC 6749: The OAuth
+ * 2.0 Authorization Framework</a> and the <a href=
+ * "https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#extensions-error">IANA
+ * OAuth Extensions Error Registry</a>. Callback handlers should communicate
+ * other problems by raising an {@code IOException}.
+ * <p>
+ * This class was introduced in 2.0.0 and, while it feels stable, it could
+ * evolve. We will try to evolve the API in a compatible manner, but we reserve
+ * the right to make breaking changes in minor releases, if necessary. We will
+ * update the {@code InterfaceStability} annotation and this notice once the API
+ * is considered stable.
+ */
+@InterfaceAudience.Public
+public class OAuthBearerValidatorCallback implements Callback {
+  private final String tokenValue;
+  private OAuthBearerToken token = null;
+  private String errorStatus = null;
+  private String errorScope = null;
+  private String errorOpenIDConfiguration = null;
+
+  /**
+   * Constructor
+   *
+   * @param tokenValue
+   *            the mandatory/non-blank token value
+   */
+  public OAuthBearerValidatorCallback(String tokenValue) {
+    if (StringUtils.isEmpty(tokenValue)) {
+      throw new IllegalArgumentException("token value must not be empty");
+    }
+    this.tokenValue = tokenValue;
+  }
+
+  /**
+   * Return the (always non-null) token value
+   *
+   * @return the (always non-null) token value
+   */
+  public String tokenValue() {
+    return tokenValue;
+  }
+
+  /**
+   * Return the (potentially null) token
+   *
+   * @return the (potentially null) token
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * Return the (potentially null) error status value as per
+   * <a href="https://tools.ietf.org/html/rfc7628#section-3.2.2">RFC 7628: A Set
+   * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth</a>
+   * and the <a href=
+   * "https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#extensions-error">IANA
+   * OAuth Extensions Error Registry</a>.
+   *
+   * @return the (potentially null) error status value
+   */
+  public String errorStatus() {
+    return errorStatus;
+  }
+
+  /**
+   * Return the (potentially null) error scope value as per
+   * <a href="https://tools.ietf.org/html/rfc7628#section-3.2.2">RFC 7628: A Set
+   * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth</a>.
+   *
+   * @return the (potentially null) error scope value
+   */
+  public String errorScope() {
+    return errorScope;
+  }
+
+  /**
+   * Return the (potentially null) error openid-configuration value as per
+   * <a href="https://tools.ietf.org/html/rfc7628#section-3.2.2">RFC 7628: A Set
+   * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth</a>.
+   *
+   * @return the (potentially null) error openid-configuration value
+   */
+  public String errorOpenIDConfiguration() {
+    return errorOpenIDConfiguration;
+  }
+
+  /**
+   * Set the token. The token value is unchanged and is expected to match the
+   * provided token's value. All error values are cleared.
+   *
+   * @param token
+   *            the mandatory token to set
+   */
+  public void token(OAuthBearerToken token) {
+    this.token = Objects.requireNonNull(token);
+    this.errorStatus = null;
+    this.errorScope = null;
+    this.errorOpenIDConfiguration = null;
+  }
+
+  /**
+   * Set the error values as per
+   * <a href="https://tools.ietf.org/html/rfc7628#section-3.2.2">RFC 7628: A Set
+   * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth</a>.
+   * Any token is cleared.
+   *
+   * @param errorStatus
+   *            the mandatory error status value from the <a href=
+   *            "https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#extensions-error">IANA
+   *            OAuth Extensions Error Registry</a> to set
+   * @param errorScope
+   *            the optional error scope value to set
+   * @param errorOpenIDConfiguration
+   *            the optional error openid-configuration value to set
+   */
+  public void error(String errorStatus, String errorScope, String errorOpenIDConfiguration) {
+    if (StringUtils.isEmpty(errorStatus)) {
+      throw new IllegalArgumentException("error status must not be empty");
+    }
+    this.errorStatus = errorStatus;
+    this.errorScope = errorScope;
+    this.errorOpenIDConfiguration = errorOpenIDConfiguration;
+    this.token = null;
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java
new file mode 100644
index 0000000..3a56d5d
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java
@@ -0,0 +1,238 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import com.nimbusds.jose.shaded.json.JSONObject;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Map;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+import javax.security.sasl.SaslServerFactory;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException;
+import org.apache.hadoop.hbase.security.SaslUtil;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code SaslServer} implementation for SASL/OAUTHBEARER in Kafka. An instance
+ * of {@link OAuthBearerToken} is available upon successful authentication via
+ * the negotiated property "{@code OAUTHBEARER.token}"; the token could be used
+ * in a custom authorizer (to authorize based on JWT claims rather than ACLs,
+ * for example).
+ */
+@InterfaceAudience.Public
+public class OAuthBearerSaslServer implements SaslServer {
+  public static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslServer.class);
+  private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAUTHBEARER_MECHANISM + ".token";
+  private static final String INTERNAL_ERROR_ON_SERVER =
+    "Authentication could not be performed due to an internal error on the server";
+  static final String CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY =
+    "CREDENTIAL.LIFETIME.MS";
+
+  private final AuthenticateCallbackHandler callbackHandler;
+
+  private boolean complete;
+  private OAuthBearerToken tokenForNegotiatedProperty = null;
+  private String errorMessage = null;
+
+  public OAuthBearerSaslServer(CallbackHandler callbackHandler) {
+    if (!(callbackHandler instanceof AuthenticateCallbackHandler)) {
+      throw new IllegalArgumentException(
+        String.format("Callback handler must be castable to %s: %s",
+          AuthenticateCallbackHandler.class.getName(), callbackHandler.getClass().getName()));
+    }
+    this.callbackHandler = (AuthenticateCallbackHandler) callbackHandler;
+  }
+
+  /**
+   * @throws SaslAuthenticationException
+   *             if access token cannot be validated
+   *             <p>
+   *             <b>Note:</b> This method may throw
+   *             {@link SaslAuthenticationException} to provide custom error
+   *             messages to clients. But care should be taken to avoid including
+   *             any information in the exception message that should not be
+   *             leaked to unauthenticated clients. It may be safer to throw
+   *             {@link SaslException} in some cases so that a standard error
+   *             message is returned to clients.
+   *             </p>
+   */
+  @Override
+  public byte[] evaluateResponse(byte[] response)
+    throws SaslException, SaslAuthenticationException {
+    try {
+      if (response.length == 1 && response[0] == OAuthBearerSaslClient.BYTE_CONTROL_A &&
+        errorMessage != null) {
+        LOG.error("Received %x01 response from client after it received our error");
+        throw new SaslAuthenticationException(errorMessage);
+      }
+      errorMessage = null;
+
+      OAuthBearerClientInitialResponse clientResponse;
+      clientResponse = new OAuthBearerClientInitialResponse(response);
+
+      return process(clientResponse.tokenValue(), clientResponse.authorizationId());
+    } catch (SaslAuthenticationException e) {
+      LOG.error("SASL authentication error", e);
+      throw e;
+    } catch (Exception e) {
+      LOG.error("SASL server problem", e);
+      throw e;
+    }
+  }
+
+  @Override
+  public String getAuthorizationID() {
+    if (!complete) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    return tokenForNegotiatedProperty.principalName();
+  }
+
+  @Override
+  public String getMechanismName() {
+    return OAUTHBEARER_MECHANISM;
+  }
+
+  @Override
+  public Object getNegotiatedProperty(String propName) {
+    if (!complete) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    if (NEGOTIATED_PROPERTY_KEY_TOKEN.equals(propName)) {
+      return tokenForNegotiatedProperty;
+    }
+    if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) {
+      return tokenForNegotiatedProperty.lifetimeMs();
+    }
+    if (Sasl.QOP.equals(propName)) {
+      return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop();
+    }
+    return null;
+  }
+
+  @Override
+  public boolean isComplete() {
+    return complete;
+  }
+
+  @Override
+  public byte[] unwrap(byte[] incoming, int offset, int len) {
+    if (!complete) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    return Arrays.copyOfRange(incoming, offset, offset + len);
+  }
+
+  @Override
+  public byte[] wrap(byte[] outgoing, int offset, int len) {
+    if (!complete) {
+      throw new IllegalStateException("Authentication exchange has not completed");
+    }
+    return Arrays.copyOfRange(outgoing, offset, offset + len);
+  }
+
+  @Override
+  public void dispose() {
+    complete = false;
+    tokenForNegotiatedProperty = null;
+  }
+
+  private byte[] process(String tokenValue, String authorizationId)
+    throws SaslException {
+    OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(tokenValue);
+    try {
+      callbackHandler.handle(new Callback[] {callback});
+    } catch (IOException | UnsupportedCallbackException e) {
+      handleCallbackError(e);
+    }
+    OAuthBearerToken token = callback.token();
+    if (token == null) {
+      errorMessage = jsonErrorResponse(callback.errorStatus(), callback.errorScope(),
+        callback.errorOpenIDConfiguration());
+      LOG.error("JWT token validation error: {}", errorMessage);
+      return errorMessage.getBytes(StandardCharsets.UTF_8);
+    }
+    /*
+     * We support the client specifying an authorization ID as per the SASL
+     * specification, but it must match the principal name if it is specified.
+     */
+    if (!authorizationId.isEmpty() && !authorizationId.equals(token.principalName())) {
+      throw new SaslAuthenticationException(String.format(
+        "Authentication failed: Client requested an authorization id (%s) that is different from "
+          + "the token's principal name (%s)",
+        authorizationId, token.principalName()));
+    }
+
+    tokenForNegotiatedProperty = token;
+    complete = true;
+    LOG.debug("Successfully authenticate User={}", token.principalName());
+    return new byte[0];
+  }
+
+  private static String jsonErrorResponse(String errorStatus, String errorScope,
+    String errorOpenIDConfiguration) {
+    JSONObject jsonObject = new JSONObject();
+    jsonObject.put("status", errorStatus);
+    if (!StringUtils.isBlank(errorScope)) {
+      jsonObject.put("scope", errorScope);
+    }
+    if (!StringUtils.isBlank(errorOpenIDConfiguration)) {
+      jsonObject.put("openid-configuration", errorOpenIDConfiguration);
+    }
+    return jsonObject.toJSONString();
+  }
+
+  private void handleCallbackError(Exception e) throws SaslException {
+    String msg = String.format("%s: %s", INTERNAL_ERROR_ON_SERVER, e.getMessage());
+    LOG.debug(msg, e);
+    throw new SaslException(msg);
+  }
+
+  public static class OAuthBearerSaslServerFactory implements SaslServerFactory {
+    @Override
+    public SaslServer createSaslServer(String mechanism, String protocol, String serverName,
+      Map<String, ?> props, CallbackHandler callbackHandler) {
+      String[] mechanismNamesCompatibleWithPolicy = getMechanismNames(props);
+      for (String s : mechanismNamesCompatibleWithPolicy) {
+        if (s.equals(mechanism)) {
+          return new OAuthBearerSaslServer(callbackHandler);
+        }
+      }
+      return null;
+    }
+
+    @Override
+    public String[] getMechanismNames(Map<String, ?> props) {
+      return OAuthBearerUtils.mechanismNamesCompatibleWithPolicy(props);
+    }
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java
new file mode 100644
index 0000000..99f7df2
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java
@@ -0,0 +1,38 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import java.security.Provider;
+import java.security.Security;
+import org.apache.yetus.audience.InterfaceAudience;
+
+@InterfaceAudience.Public
+public class OAuthBearerSaslServerProvider extends Provider {
+  private static final long serialVersionUID = 1L;
+
+  protected OAuthBearerSaslServerProvider() {
+    super("SASL/OAUTHBEARER Server Provider", 1.0, "SASL/OAUTHBEARER Server Provider for HBase");
+    put("SaslServerFactory." + OAUTHBEARER_MECHANISM,
+      OAuthBearerSaslServer.OAuthBearerSaslServerFactory.class.getName());
+  }
+
+  public static void initialize() {
+    Security.addProvider(new OAuthBearerSaslServerProvider());
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java
new file mode 100644
index 0000000..acd5b04
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Exception thrown when there is a problem with the configuration (an invalid
+ * option in a JAAS config, for example).
+ */
+@InterfaceAudience.Public
+public class OAuthBearerConfigException extends RuntimeException {
+  private static final long serialVersionUID = -8056105648062343518L;
+
+  public OAuthBearerConfigException(String s) {
+    super(s);
+  }
+
+  public OAuthBearerConfigException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java
new file mode 100644
index 0000000..09aa28c
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java
@@ -0,0 +1,65 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import java.util.Objects;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Exception thrown when token validation fails due to a problem with the token
+ * itself (as opposed to a missing remote resource or a configuration problem)
+ */
+@InterfaceAudience.Public
+public class OAuthBearerIllegalTokenException extends RuntimeException {
+  private static final long serialVersionUID = -5275276640051316350L;
+  private final OAuthBearerValidationResult reason;
+
+  /**
+   * Constructor
+   *
+   * @param reason
+   *            the mandatory reason for the validation failure; it must indicate
+   *            failure
+   */
+  public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason) {
+    super(Objects.requireNonNull(reason, "Reason cannot be null").failureDescription());
+    if (reason.success()) {
+      throw new IllegalArgumentException(
+        "The reason indicates success; it must instead indicate failure");
+    }
+    this.reason = reason;
+  }
+
+  public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason, Throwable t) {
+    super(Objects.requireNonNull(reason, "Reason cannot be null").failureDescription(), t);
+    if (reason.success()) {
+      throw new IllegalArgumentException(
+        "The reason indicates success; it must instead indicate failure");
+    }
+    this.reason = reason;
+  }
+
+  /**
+   * Return the (always non-null) reason for the validation failure
+   *
+   * @return the reason for the validation failure
+   */
+  public OAuthBearerValidationResult reason() {
+    return reason;
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java
new file mode 100644
index 0000000..f17457d
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java
@@ -0,0 +1,193 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.JWTParser;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Signed JWT implementation for OAuth Bearer authentication mech of SASL.
+ *
+ * This class is based on Kafka's Unsecured JWS token implementation.
+ */
+@InterfaceAudience.Public
+public class OAuthBearerSignedJwt implements OAuthBearerToken {
+  private final String compactSerialization;
+  private final JWKSet jwkSet;
+
+  private JWTClaimsSet claims;
+  private long lifetime;
+  private int maxClockSkewSeconds = 0;
+  private String requiredAudience;
+  private String requiredIssuer;
+
+  /**
+   * Constructor base64 encoded JWT token and JWK Set.
+   *
+   * @param compactSerialization
+   *            the compact serialization to parse as a signed JWT
+   * @param jwkSet
+   *            the key set which the signature of this JWT should be verified with
+   */
+  public OAuthBearerSignedJwt(String compactSerialization, JWKSet jwkSet) {
+    this.jwkSet = jwkSet;
+    this.compactSerialization = Objects.requireNonNull(compactSerialization);
+  }
+
+  @Override
+  public String value() {
+    return compactSerialization;
+  }
+
+  @Override
+  public String principalName() {
+    return claims.getSubject();
+  }
+
+  @Override
+  public long lifetimeMs() {
+    return lifetime;
+  }
+
+  /**
+   * Return the JWT Claim Set as a {@code Map}
+   *
+   * @return the (always non-null but possibly empty) claims
+   */
+  public Map<String, Object> claims() {
+    return claims.getClaims();
+  }
+
+  /**
+   * Set required audience, as per
+   * <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3">
+   *   RFC7519 Section 4.1.3</a>
+   */
+  public OAuthBearerSignedJwt audience(String aud) {
+    this.requiredAudience = aud;
+    return this;
+  }
+
+  /**
+   * Set required issuer, as per
+   * <a href="https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1">
+   *   RFC7519 Section 4.1.1</a>
+   */
+  public OAuthBearerSignedJwt issuer(String iss) {
+    this.requiredIssuer = iss;
+    return this;
+  }
+
+  /**
+   * Set maximum clock skew in seconds.
+   * @param value New value
+   */
+  public OAuthBearerSignedJwt maxClockSkewSeconds(int value) {
+    this.maxClockSkewSeconds = value;
+    return this;
+  }
+
+  /**
+   * This method provides a single method for validating the JWT for use in
+   * request processing.
+   *
+   * @throws OAuthBearerIllegalTokenException
+   *             if the compact serialization is not a valid JWT
+   *             (meaning it did not have 3 dot-separated Base64URL sections
+   *             with a digital signature; or the header or claims
+   *             either are not valid Base 64 URL encoded values or are not JSON
+   *             after decoding; or the mandatory '{@code alg}' header value is
+   *             missing)
+   */
+  public OAuthBearerSignedJwt validate(){
+    try {
+      this.claims = validateToken(compactSerialization);
+      Date expirationTimeSeconds = claims.getExpirationTime();
+      if (expirationTimeSeconds == null) {
+        throw new OAuthBearerIllegalTokenException(
+          OAuthBearerValidationResult.newFailure("No expiration time in JWT"));
+      }
+      lifetime = expirationTimeSeconds.toInstant().toEpochMilli();
+      String principalName = claims.getSubject();
+      if (StringUtils.isBlank(principalName)) {
+        throw new OAuthBearerIllegalTokenException(OAuthBearerValidationResult
+          .newFailure("No principal name in JWT claim"));
+      }
+      return this;
+    } catch (ParseException | BadJOSEException | JOSEException e) {
+      throw new OAuthBearerIllegalTokenException(
+        OAuthBearerValidationResult.newFailure("Token validation failed: " + e.getMessage()), e);
+    }
+  }
+
+  private JWTClaimsSet validateToken(String jwtToken)
+    throws BadJOSEException, JOSEException, ParseException {
+    JWT jwt = JWTParser.parse(jwtToken);
+    ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
+
+    Set<String> requiredClaims = new HashSet<>();
+    JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder();
+
+    // Audience
+    if (!StringUtils.isBlank(requiredAudience)) {
+      requiredClaims.add("aud");
+      jwtClaimsSetBuilder.audience(requiredAudience);
+    }
+
+    // Issuer
+    if (!StringUtils.isBlank(requiredIssuer)) {
+      requiredClaims.add("iss");
+      jwtClaimsSetBuilder.issuer(requiredIssuer);
+    }
+
+    // Subject / Principal is always required
+    requiredClaims.add("sub");
+
+    DefaultJWTClaimsVerifier<SecurityContext> jwtClaimsSetVerifier =
+      new DefaultJWTClaimsVerifier<>(jwtClaimsSetBuilder.build(), requiredClaims);
+    jwtClaimsSetVerifier.setMaxClockSkew(maxClockSkewSeconds);
+    jwtProcessor.setJWTClaimsSetVerifier(jwtClaimsSetVerifier);
+
+    JWSKeySelector<SecurityContext> keySelector =
+      new JWSVerificationKeySelector<>((JWSAlgorithm)jwt.getHeader().getAlgorithm(),
+        new ImmutableJWKSet<>(jwkSet));
+    jwtProcessor.setJWSKeySelector(keySelector);
+    return jwtProcessor.process(jwtToken, null);
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java
new file mode 100644
index 0000000..8fb33ef
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java
@@ -0,0 +1,196 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM;
+import com.nimbusds.jose.jwk.JWKSet;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@code CallbackHandler} that recognizes
+ * {@link OAuthBearerValidatorCallback} and validates a secure (signed) OAuth 2
+ * bearer token (JWT).
+ *
+ * It requires a valid JWK Set to be initialized at startup which holds the available
+ * RSA public keys that JWT signature can be validated with. The Set can be initialized
+ * via an URL or a local file.
+ *
+ * It requires there to be an <code>"exp" (Expiration Time)</code>
+ * claim of type Number. If <code>"iat" (Issued At)</code> or
+ * <code>"nbf" (Not Before)</code> claims are present each must be a number that
+ * precedes the Expiration Time claim, and if both are present the Not Before
+ * claim must not precede the Issued At claim. It also accepts the following
+ * options, none of which are required:
+ * <ul>
+ * <li>{@code hbase.security.oauth.jwt.jwks.url} set to a non-empty value if you
+ * wish to initialize the JWK Set via an URL. HTTPS URLs must have valid certificates.
+ * </li>
+ * <li>{@code hbase.security.oauth.jwt.jwks.file} set to a non-empty value if you
+ * wish to initialize the JWK Set from a local JSON file.
+ * </li>
+ * <li>{@code hbase.security.oauth.jwt.audience} set to a String value which
+ * you want the desired audience ("aud") the JWT to have.</li>
+ * <li>{@code hbase.security.oauth.jwt.issuer} set to a String value which
+ * you want the issuer ("iss") of the JWT has to be.</li>
+ * <li>{@code hbase.security.oauth.jwt.allowableclockskewseconds} set to a positive integer
+ * value if you wish to allow up to some number of positive seconds of
+ * clock skew (the default is 0)</li>
+ * </ul>
+ *
+ * This class is based on Kafka's OAuthBearerUnsecuredValidatorCallbackHandler.
+ */
+@InterfaceAudience.Public
+public class OAuthBearerSignedJwtValidatorCallbackHandler implements AuthenticateCallbackHandler {
+  private static final Logger LOG =
+    LoggerFactory.getLogger(OAuthBearerSignedJwtValidatorCallbackHandler.class);
+  private static final String OPTION_PREFIX = "hbase.security.oauth.jwt.";
+  private static final String JWKS_URL = OPTION_PREFIX + "jwks.url";
+  private static final String JWKS_FILE = OPTION_PREFIX + "jwks.file";
+  private static final String ALLOWABLE_CLOCK_SKEW_SECONDS_OPTION =
+    OPTION_PREFIX + "allowableclockskewseconds";
+  static final String REQUIRED_AUDIENCE_OPTION = OPTION_PREFIX + "audience";
+  static final String REQUIRED_ISSUER_OPTION = OPTION_PREFIX + "issuer";
+  private Configuration hBaseConfiguration;
+  private JWKSet jwkSet;
+  private boolean configured = false;
+
+  @Override
+  public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
+    if (!configured) {
+      throw new RuntimeException(
+        "OAuthBearerSignedJwtValidatorCallbackHandler must be configured first.");
+    }
+
+    for (Callback callback : callbacks) {
+      if (callback instanceof OAuthBearerValidatorCallback) {
+        OAuthBearerValidatorCallback validationCallback = (OAuthBearerValidatorCallback) callback;
+        try {
+          handleCallback(validationCallback);
+        } catch (OAuthBearerIllegalTokenException e) {
+          LOG.error("Signed JWT token validation error: {}", e.getMessage());
+          OAuthBearerValidationResult failureReason = e.reason();
+          String failureScope = failureReason.failureScope();
+          validationCallback.error(failureScope != null ? "insufficient_scope" : "invalid_token",
+            failureScope, failureReason.failureOpenIdConfig());
+        }
+      } else {
+        throw new UnsupportedCallbackException(callback);
+      }
+    }
+  }
+
+  @Override public void configure(Configuration configs, String saslMechanism,
+    Map<String, String> saslProps) {
+    if (!OAUTHBEARER_MECHANISM.equals(saslMechanism)) {
+      throw new IllegalArgumentException(
+        String.format("Unexpected SASL mechanism: %s", saslMechanism));
+    }
+
+    this.hBaseConfiguration = configs;
+
+    try {
+      loadJwkSet();
+    } catch (IOException | ParseException e) {
+      throw new RuntimeException("Unable to initialize JWK Set", e);
+    }
+
+    configured = true;
+  }
+
+  @InterfaceAudience.Private
+  public void configure(Configuration configs, JWKSet jwkSet) {
+    this.hBaseConfiguration = Objects.requireNonNull(configs);
+    this.jwkSet = Objects.requireNonNull(jwkSet);
+    this.configured = true;
+  }
+
+  private void handleCallback(OAuthBearerValidatorCallback callback) {
+    String tokenValue = callback.tokenValue();
+    if (tokenValue == null) {
+      throw new IllegalArgumentException("Callback missing required token value");
+    }
+    OAuthBearerSignedJwt signedJwt = new OAuthBearerSignedJwt(tokenValue, jwkSet)
+      .audience(requiredAudience())
+      .issuer(requiredIssuer())
+      .maxClockSkewSeconds(allowableClockSkewSeconds())
+      .validate();
+
+    LOG.info("Successfully validated token with principal {}: {}", signedJwt.principalName(),
+      signedJwt.claims());
+    callback.token(signedJwt);
+  }
+
+  private String requiredAudience() {
+    return hBaseConfiguration.get(REQUIRED_AUDIENCE_OPTION);
+  }
+
+  private String requiredIssuer() {
+    return hBaseConfiguration.get(REQUIRED_ISSUER_OPTION);
+  }
+
+  private int allowableClockSkewSeconds() {
+    String allowableClockSkewSecondsValue = hBaseConfiguration.get(
+      ALLOWABLE_CLOCK_SKEW_SECONDS_OPTION);
+    int allowableClockSkewSeconds = 0;
+    try {
+      allowableClockSkewSeconds = StringUtils.isBlank(allowableClockSkewSecondsValue)
+        ? 0 : Integer.parseInt(allowableClockSkewSecondsValue.trim());
+    } catch (NumberFormatException e) {
+      throw new OAuthBearerConfigException(e.getMessage(), e);
+    }
+    if (allowableClockSkewSeconds < 0) {
+      throw new OAuthBearerConfigException(
+        String.format("Allowable clock skew seconds must not be negative: %s",
+          allowableClockSkewSecondsValue));
+    }
+    return allowableClockSkewSeconds;
+  }
+
+  private void loadJwkSet() throws IOException, ParseException {
+    String jwksFile = hBaseConfiguration.get(JWKS_FILE);
+    String jwksUrl = hBaseConfiguration.get(JWKS_URL);
+
+    if (StringUtils.isBlank(jwksFile) && StringUtils.isBlank(jwksUrl)) {
+      throw new RuntimeException("Failed to initialize JWKS db. "
+        + JWKS_FILE + " or " + JWKS_URL + " must be specified in the config.");
+    }
+
+    if (!StringUtils.isBlank(jwksFile)) {
+      this.jwkSet = JWKSet.load(new File(jwksFile));
+      LOG.debug("JWKS db initialized from file: {}", jwksFile);
+      return;
+    }
+
+    this.jwkSet = JWKSet.load(new URL(jwksUrl));
+    LOG.debug("JWKS db initialized from URL: {}", jwksUrl);
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java
new file mode 100644
index 0000000..d41962d
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java
@@ -0,0 +1,135 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import java.io.Serializable;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * The result of some kind of token validation
+ *
+ * This class has been copy-and-pasted from Kafka codebase.
+ */
+@InterfaceAudience.Public
+public final class OAuthBearerValidationResult implements Serializable {
+  private static final long serialVersionUID = 5774669940899777373L;
+  private final boolean success;
+  private final String failureDescription;
+  private final String failureScope;
+  private final String failureOpenIdConfig;
+
+  /**
+   * Return an instance indicating success
+   *
+   * @return an instance indicating success
+   */
+  public static OAuthBearerValidationResult newSuccess() {
+    return new OAuthBearerValidationResult(true, null, null, null);
+  }
+
+  /**
+   * Return a new validation failure instance
+   *
+   * @param failureDescription
+   *            optional description of the failure
+   * @return a new validation failure instance
+   */
+  public static OAuthBearerValidationResult newFailure(String failureDescription) {
+    return newFailure(failureDescription, null, null);
+  }
+
+  /**
+   * Return a new validation failure instance
+   *
+   * @param failureDescription
+   *            optional description of the failure
+   * @param failureScope
+   *            optional scope to be reported with the failure
+   * @param failureOpenIdConfig
+   *            optional OpenID Connect configuration to be reported with the
+   *            failure
+   * @return a new validation failure instance
+   */
+  public static OAuthBearerValidationResult newFailure(String failureDescription,
+    String failureScope, String failureOpenIdConfig) {
+    return new OAuthBearerValidationResult(false, failureDescription, failureScope,
+      failureOpenIdConfig);
+  }
+
+  private OAuthBearerValidationResult(boolean success, String failureDescription,
+    String failureScope, String failureOpenIdConfig) {
+    if (success && (failureScope != null || failureOpenIdConfig != null)) {
+      throw new IllegalArgumentException(
+        "success was indicated but failure scope/OpenIdConfig were provided");
+    }
+    this.success = success;
+    this.failureDescription = failureDescription;
+    this.failureScope = failureScope;
+    this.failureOpenIdConfig = failureOpenIdConfig;
+  }
+
+  /**
+   * Return true if this instance indicates success, otherwise false
+   *
+   * @return true if this instance indicates success, otherwise false
+   */
+  public boolean success() {
+    return success;
+  }
+
+  /**
+   * Return the (potentially null) descriptive message for the failure
+   *
+   * @return the (potentially null) descriptive message for the failure
+   */
+  public String failureDescription() {
+    return failureDescription;
+  }
+
+  /**
+   * Return the (potentially null) scope to be reported with the failure
+   *
+   * @return the (potentially null) scope to be reported with the failure
+   */
+  public String failureScope() {
+    return failureScope;
+  }
+
+  /**
+   * Return the (potentially null) OpenID Connect configuration to be reported
+   * with the failure
+   *
+   * @return the (potentially null) OpenID Connect configuration to be reported
+   *         with the failure
+   */
+  public String failureOpenIdConfig() {
+    return failureOpenIdConfig;
+  }
+
+  /**
+   * Raise an exception if this instance indicates failure, otherwise do nothing
+   *
+   * @throws OAuthBearerIllegalTokenException
+   *             if this instance indicates failure
+   */
+  public void throwExceptionIfFailed() throws OAuthBearerIllegalTokenException {
+    if (!success()) {
+      throw new OAuthBearerIllegalTokenException(this);
+    }
+  }
+}
diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java
new file mode 100644
index 0000000..b6f8078
--- /dev/null
+++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java
@@ -0,0 +1,100 @@
+/*
+ *  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.hadoop.hbase.security.provider;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Map;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslException;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslServerProvider;
+import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.hadoop.security.token.SecretManager;
+import org.apache.hadoop.security.token.TokenIdentifier;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@InterfaceAudience.Private
+public class OAuthBearerSaslServerAuthenticationProvider
+    extends OAuthBearerSaslAuthenticationProvider
+    implements SaslServerAuthenticationProvider {
+
+  private static final Logger LOG = LoggerFactory.getLogger(
+    OAuthBearerSaslServerAuthenticationProvider.class);
+  private Configuration hbaseConfiguration;
+  private boolean initialized = false;
+
+  static {
+    OAuthBearerSaslServerProvider.initialize(); // not part of public API
+    LOG.info("OAuthBearer SASL server provider has been initialized");
+  }
+
+  @Override public void init(Configuration conf) throws IOException {
+    this.hbaseConfiguration = conf;
+    this.initialized = true;
+  }
+
+  @Override public AttemptingUserProvidingSaslServer createServer(
+    SecretManager<TokenIdentifier> secretManager, Map<String, String> saslProps)
+    throws IOException {
+
+    if (!initialized) {
+      throw new IllegalStateException(
+        "OAuthBearerSaslServerAuthenticationProvider must be initialized first.");
+    }
+
+    UserGroupInformation current = UserGroupInformation.getCurrentUser();
+    String fullName = current.getUserName();
+    LOG.debug("Server's OAuthBearer user name is {}", fullName);
+    LOG.debug("OAuthBearer saslProps = {}", saslProps);
+
+    try {
+      return current.doAs(new PrivilegedExceptionAction<AttemptingUserProvidingSaslServer>() {
+        @Override
+        public AttemptingUserProvidingSaslServer run() throws SaslException {
+          AuthenticateCallbackHandler callbackHandler =
+            new OAuthBearerSignedJwtValidatorCallbackHandler();
+          callbackHandler.configure(hbaseConfiguration, getSaslAuthMethod().getSaslMechanism(),
+            saslProps);
+          return new AttemptingUserProvidingSaslServer(Sasl.createSaslServer(
+            getSaslAuthMethod().getSaslMechanism(), null, null, saslProps,
+            callbackHandler), () -> null);
+        }
+      });
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new InterruptedIOException("Failed to construct OAUTHBEARER SASL server");
+    }
+  }
+
+  @Override public boolean supportsProtocolAuthentication() {
+    return true;
+  }
+
+  @Override public UserGroupInformation getAuthorizedUgi(String authzId,
+    SecretManager<TokenIdentifier> secretManager) {
+    UserGroupInformation ugi = UserGroupInformation.createRemoteUser(authzId);
+    ugi.setAuthenticationMethod(getSaslAuthMethod().getAuthMethod());
+    return ugi;
+  }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java
new file mode 100644
index 0000000..9e6670b
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java
@@ -0,0 +1,34 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+public class OAuthBearerTokenMock implements OAuthBearerToken {
+  @Override
+  public String value() {
+    return null;
+  }
+
+  @Override
+  public long lifetimeMs() {
+    return 0;
+  }
+
+  @Override
+  public String principalName() {
+    return null;
+  }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java
new file mode 100644
index 0000000..8be3cfc
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerValidatorCallbackTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerValidatorCallbackTest.class);
+
+  private static final OAuthBearerToken TOKEN = new OAuthBearerToken() {
+    @Override
+    public String value() {
+      return "value";
+    }
+
+    @Override
+    public String principalName() {
+      return "principalName";
+    }
+
+    @Override
+    public long lifetimeMs() {
+      return 0;
+    }
+  };
+
+  @Test
+  public void testError() {
+    String errorStatus = "errorStatus";
+    String errorScope = "errorScope";
+    String errorOpenIDConfiguration = "errorOpenIDConfiguration";
+    OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(TOKEN.value());
+    callback.error(errorStatus, errorScope, errorOpenIDConfiguration);
+    assertEquals(errorStatus, callback.errorStatus());
+    assertEquals(errorScope, callback.errorScope());
+    assertEquals(errorOpenIDConfiguration, callback.errorOpenIDConfiguration());
+    assertNull(callback.token());
+  }
+
+  @Test
+  public void testToken() {
+    OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(TOKEN.value());
+    callback.token(TOKEN);
+    assertSame(TOKEN, callback.token());
+    assertNull(callback.errorStatus());
+    assertNull(callback.errorScope());
+    assertNull(callback.errorOpenIDConfiguration());
+  }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java
new file mode 100644
index 0000000..377371e
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils.USER;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import java.nio.charset.StandardCharsets;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException;
+import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerConfigException;
+import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerSaslServerTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerSaslServerTest.class);
+
+  private static final Configuration CONFIGS;
+  static {
+    CONFIGS = new Configuration();
+  }
+
+  private String JWT;
+  private OAuthBearerSaslServer saslServer;
+
+  @Before
+  public void setUp() throws JOSEException {
+    RSAKey rsaKey = JwtTestUtils.generateRSAKey();
+    JWT = JwtTestUtils.createSignedJwt(rsaKey);
+    OAuthBearerSignedJwtValidatorCallbackHandler validatorCallbackHandler =
+      new OAuthBearerSignedJwtValidatorCallbackHandler();
+    validatorCallbackHandler.configure(CONFIGS, new JWKSet(rsaKey));
+    // only validate extensions "firstKey" and "secondKey"
+    saslServer = new OAuthBearerSaslServer(validatorCallbackHandler);
+  }
+
+  @Test
+  public void noAuthorizationIdSpecified() throws Exception {
+    byte[] nextChallenge = saslServer
+      .evaluateResponse(clientInitialResponse(null));
+    // also asserts that no authentication error is thrown
+    // if OAuthBearerExtensionsValidatorCallback is not supported
+    assertTrue("Next challenge is not empty",nextChallenge.length == 0);
+  }
+
+  @Test
+  public void negotiatedProperty() throws Exception {
+    saslServer.evaluateResponse(clientInitialResponse(USER));
+    OAuthBearerToken token =
+      (OAuthBearerToken) saslServer.getNegotiatedProperty("OAUTHBEARER.token");
+    assertNotNull(token);
+    assertEquals(token.lifetimeMs(),
+      saslServer.getNegotiatedProperty(
+        OAuthBearerSaslServer.CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY));
+  }
+
+  @Test
+  public void authorizatonIdEqualsAuthenticationId() throws Exception {
+    byte[] nextChallenge = saslServer
+      .evaluateResponse(clientInitialResponse(USER));
+    assertTrue("Next challenge is not empty", nextChallenge.length == 0);
+  }
+
+  @Test
+  public void authorizatonIdNotEqualsAuthenticationId() {
+    assertThrows(SaslAuthenticationException.class,
+      () -> saslServer.evaluateResponse(clientInitialResponse(USER + "x")));
+  }
+
+  @Test
+  public void illegalToken() throws Exception {
+    byte[] bytes = saslServer.evaluateResponse(clientInitialResponse(null, true));
+    String challenge = new String(bytes, StandardCharsets.UTF_8);
+    assertEquals("{\"status\":\"invalid_token\"}", challenge);
+  }
+
+  private byte[] clientInitialResponse(String authorizationId)
+    throws OAuthBearerConfigException {
+    return clientInitialResponse(authorizationId, false);
+  }
+
+  private byte[] clientInitialResponse(String authorizationId, boolean illegalToken)
+    throws OAuthBearerConfigException {
+    String compactSerialization = JWT;
+    String tokenValue = compactSerialization + (illegalToken ? "AB" : "");
+    return new OAuthBearerClientInitialResponse(tokenValue, authorizationId).toBytes();
+  }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java
new file mode 100644
index 0000000..4b72f88
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerSignedJwtTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerSignedJwtTest.class);
+  private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
+  private final static int EXP_DAYS = 10;
+
+  private JWKSet JWK_SET;
+  private RSAKey RSA_KEY;
+
+  @Before
+  public void before() throws JOSEException {
+    RSA_KEY = JwtTestUtils.generateRSAKey();
+    JWK_SET = new JWKSet(RSA_KEY);
+  }
+
+  @Test
+  public void validCompactSerialization() throws JOSEException {
+    String subject = "foo";
+
+    LocalDate issuedAt = LocalDate.now(ZONE_ID);
+    LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS);
+    String validCompactSerialization =
+      compactSerialization(subject, issuedAt, expirationTime);
+    OAuthBearerSignedJwt jws = new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET)
+      .validate();
+    assertEquals(5, jws.claims().size());
+    assertEquals(subject, jws.claims().get("sub"));
+    assertEquals(issuedAt,  Date.class.cast(jws.claims().get("iat")).toInstant()
+      .atZone(ZoneId.systemDefault()).toLocalDate());
+    assertEquals(expirationTime, Date.class.cast(jws.claims().get("exp")).toInstant()
+      .atZone(ZoneId.systemDefault()).toLocalDate());
+    assertEquals(expirationTime.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(),
+      jws.lifetimeMs());
+  }
+
+  @Test
+  public void missingPrincipal() throws JOSEException {
+    String subject = null;
+    LocalDate issuedAt = LocalDate.now(ZONE_ID);
+    LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS);
+    String validCompactSerialization =
+      compactSerialization(subject, issuedAt, expirationTime);
+    assertThrows(OAuthBearerIllegalTokenException.class,
+      () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET).validate());
+  }
+
+  @Test
+  public void blankPrincipalName() throws JOSEException {
+    String subject = "   ";
+    LocalDate issuedAt = LocalDate.now(ZONE_ID);
+    LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS);
+    String validCompactSerialization =
+      compactSerialization(subject, issuedAt, expirationTime);
+    assertThrows(OAuthBearerIllegalTokenException.class,
+      () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET).validate());
+  }
+
+  @Test
+  public void missingIssuer() throws JOSEException {
+    String validCompactSerialization =
+      JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "");
+    assertThrows(OAuthBearerIllegalTokenException.class,
+      () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET)
+        .issuer("test-issuer")
+        .validate());
+  }
+
+  @Test
+  public void badIssuer() throws JOSEException {
+    String validCompactSerialization =
+      JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "bad-issuer");
+    assertThrows(OAuthBearerIllegalTokenException.class,
+      () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET)
+        .issuer("test-issuer")
+        .validate());
+  }
+
+  private String compactSerialization(String subject, LocalDate issuedAt, LocalDate expirationTime)
+    throws JOSEException {
+    return JwtTestUtils.createSignedJwt(RSA_KEY, "me", subject,
+        expirationTime, issuedAt, "test-audience");
+  }
+}
diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java
new file mode 100644
index 0000000..aa6c8e6
--- /dev/null
+++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.hadoop.hbase.security.oauthbearer.internals.knox;
+
+import static org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler.REQUIRED_AUDIENCE_OPTION;
+import static org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler.REQUIRED_ISSUER_OPTION;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.hadoop.hbase.HBaseClassTestRule;
+import org.apache.hadoop.hbase.HBaseConfiguration;
+import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+@Category({ MiscTests.class, SmallTests.class})
+public class OAuthBearerSignedJwtValidatorCallbackHandlerTest {
+  @ClassRule
+  public static final HBaseClassTestRule CLASS_RULE =
+    HBaseClassTestRule.forClass(OAuthBearerSignedJwtValidatorCallbackHandlerTest.class);
+
+  private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles");
+  private static final HBaseConfiguration EMPTY_CONFIG = new HBaseConfiguration();
+  private static final HBaseConfiguration REQUIRED_AUDIENCE_CONFIG;
+  static {
+    REQUIRED_AUDIENCE_CONFIG = new HBaseConfiguration();
+    REQUIRED_AUDIENCE_CONFIG.set(REQUIRED_AUDIENCE_OPTION, "test-audience");
+  }
+  private static final HBaseConfiguration REQUIRED_ISSUER_CONFIG;
+  static {
+    REQUIRED_ISSUER_CONFIG = new HBaseConfiguration();
+    REQUIRED_ISSUER_CONFIG.set(REQUIRED_ISSUER_OPTION, "test-issuer");
+  }
+
+  private RSAKey RSA_KEY;
+
+  @Before
+  public void before() throws JOSEException {
+    RSA_KEY = JwtTestUtils.generateRSAKey();
+  }
+
+  @Test
+  public void validToken() throws JOSEException, UnsupportedCallbackException {
+    Object validationResult = validationResult(EMPTY_CONFIG, JwtTestUtils.createSignedJwt(RSA_KEY));
+    assertTrue(validationResult instanceof OAuthBearerValidatorCallback);
+    assertTrue(((OAuthBearerValidatorCallback) validationResult).token()
+      instanceof OAuthBearerSignedJwt);
+  }
+
+  @Test
+  public void missingPrincipal()
+    throws UnsupportedCallbackException, JOSEException {
+    LocalDate now = LocalDate.now(ZONE_ID);
+    String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "",
+      now.plusDays(1), now, "test-aud");
+    confirmFailsValidation(EMPTY_CONFIG, token);
+  }
+
+  @Test
+  public void tooEarlyExpirationTime() throws JOSEException, UnsupportedCallbackException {
+    LocalDate now = LocalDate.now(ZONE_ID);
+    String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "",
+      now.minusDays(1),
+      now.minusDays(1),
+      "test-aud");
+    confirmFailsValidation(EMPTY_CONFIG, token);
+  }
+
+  @Test
+  public void requiredAudience() throws JOSEException, UnsupportedCallbackException {
+    String token = JwtTestUtils.createSignedJwtWithAudience(RSA_KEY, "test-audience");
+    Object validationResult = validationResult(REQUIRED_AUDIENCE_CONFIG, token);
+    assertTrue(validationResult instanceof OAuthBearerValidatorCallback);
+    assertTrue(((OAuthBearerValidatorCallback) validationResult).token()
+      instanceof OAuthBearerSignedJwt);
+  }
+
+  @Test
+  public void missingAudience() throws JOSEException, UnsupportedCallbackException {
+    String token = JwtTestUtils.createSignedJwt(RSA_KEY);
+    confirmFailsValidation(REQUIRED_AUDIENCE_CONFIG, token);
+  }
+
+  @Test
+  public void badAudience() throws JOSEException, UnsupportedCallbackException {
+    String token = JwtTestUtils.createSignedJwtWithAudience(RSA_KEY, "bad-audience");
+    confirmFailsValidation(REQUIRED_AUDIENCE_CONFIG, token);
+  }
+
+  @Test
+  public void requiredIssuer() throws UnsupportedCallbackException, JOSEException {
+    String token = JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "test-issuer");
+    Object validationResult = validationResult(REQUIRED_ISSUER_CONFIG, token);
+    assertTrue(validationResult instanceof OAuthBearerValidatorCallback);
+    assertTrue(((OAuthBearerValidatorCallback) validationResult).token()
+      instanceof OAuthBearerSignedJwt);
+  }
+
+  @Test
+  public void missingIssuer() throws JOSEException, UnsupportedCallbackException {
+    String token = JwtTestUtils.createSignedJwt(RSA_KEY);
+    confirmFailsValidation(REQUIRED_ISSUER_CONFIG, token);
+  }
+
+  @Test
+  public void badIssuer() throws JOSEException, UnsupportedCallbackException {
+    String token = JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "bad-issuer");
+    confirmFailsValidation(REQUIRED_ISSUER_CONFIG, token);
+  }
+
+  private void confirmFailsValidation(HBaseConfiguration config, String tokenValue)
+    throws OAuthBearerConfigException, OAuthBearerIllegalTokenException,
+    UnsupportedCallbackException {
+    Object validationResultObj = validationResult(config, tokenValue);
+    assertTrue(validationResultObj instanceof OAuthBearerValidatorCallback);
+    OAuthBearerValidatorCallback callback = (OAuthBearerValidatorCallback) validationResultObj;
+    assertNull(callback.token());
+    assertNull(callback.errorOpenIDConfiguration());
+    assertEquals("invalid_token", callback.errorStatus());
+    assertNull(callback.errorScope());
+  }
+
+  private OAuthBearerValidatorCallback validationResult(HBaseConfiguration config,
+    String tokenValue)
+    throws UnsupportedCallbackException {
+    OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(tokenValue);
+    createCallbackHandler(config).handle(new Callback[] {callback});
+    return callback;
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private OAuthBearerSignedJwtValidatorCallbackHandler
+    createCallbackHandler(HBaseConfiguration config) {
+    OAuthBearerSignedJwtValidatorCallbackHandler callbackHandler =
+      new OAuthBearerSignedJwtValidatorCallbackHandler();
+    callbackHandler.configure(config, new JWKSet(RSA_KEY));
+    return callbackHandler;
+  }
+}
diff --git a/pom.xml b/pom.xml
index 170b95c..7983cfa 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1841,6 +1841,7 @@
     <xz.version>1.9</xz.version>
     <zstd-jni.version>1.5.0-4</zstd-jni.version>
     <hbase-thirdparty.version>4.0.1</hbase-thirdparty.version>
+    <nimbusds.version>9.15.2</nimbusds.version>
     <!-- Intraproject jar naming properties -->
     <!-- TODO this is pretty ugly, but works for the moment.
       Modules are pretty heavy-weight things, so doing this work isn't too bad. -->
@@ -3441,12 +3442,16 @@
            <artifactId>hadoop-distcp</artifactId>
            <version>${hadoop-three.version}</version>
          </dependency>
-
          <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs-client</artifactId>
            <version>${hadoop-three.version}</version>
          </dependency>
+         <dependency>
+           <groupId>com.nimbusds</groupId>
+           <artifactId>nimbus-jose-jwt</artifactId>
+           <version>${nimbusds.version}</version>
+         </dependency>
        </dependencies>
      </dependencyManagement>