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 000000000000..a9e28efbc2a8
--- /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
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework . Callback handlers should communicate other
+ * problems by raising an {@code IOException}.
+ *
+ * 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
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework .
+ *
+ * @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
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework .
+ *
+ * @return the (potentially null) error description
+ */
+ public String errorDescription() {
+ return errorDescription;
+ }
+
+ /**
+ * Return the (potentially null) error URI as per
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework .
+ *
+ * @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
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework . 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 000000000000..df3d08a081d1
--- /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 RFC 6750 Section 2.1
+ *
+ * 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 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 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 000000000000..3ba721779e99
--- /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 000000000000..8b4dcfe5c75b
--- /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 000000000000..915706c1027b
--- /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 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 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 privateCredentials = subject != null
+ ? subject.getPrivateCredentials(OAuthBearerToken.class)
+ : Collections.emptySet();
+ callback.token(choosePrivateCredential(privateCredentials));
+ }
+
+ private OAuthBearerToken choosePrivateCredential(Set 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 sortedByLifetime =
+ new TreeSet<>(
+ new Comparator() {
+ @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 000000000000..88c2eed0c953
--- /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 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> selectProvider(
+ String clusterId, User user) {
+ Pair> pair =
+ super.selectProvider(clusterId, user);
+
+ Optional> 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 000000000000..d9e42d5973b4
--- /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() {
+ @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 000000000000..06ce6577ce5d
--- /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 000000000000..0267084d35e7
--- /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 000000000000..caf85db44faa
--- /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) () -> {
+ 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) () -> {
+ final int maxTokens = 4;
+ final Set 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 000000000000..ce7d1f7c3ddb
--- /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 000000000000..3f4866e0f557
--- /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.
+ *
+ * SASL authentication failures typically indicate invalid credentials, but
+ * could also include other failures specific to the SASL mechanism used
+ * for authentication.
+ *
+ * Note: 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.
+ *
+ */
+@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 000000000000..1329e9ac67f7
--- /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 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 000000000000..769bceea6181
--- /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 b64token value as defined in
+ * RFC 6750 Section
+ * 2.1 along with the token's specific scope and lifetime and principal
+ * name.
+ *
+ * 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 RFC 6749
+ * Section 1.4 and
+ * RFC 6750
+ * Section 2.1
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+public interface OAuthBearerToken {
+ /**
+ * The b64token value as defined in
+ * RFC 6750 Section
+ * 2.1
+ *
+ * @return b64token value as defined in
+ * RFC 6750
+ * Section 2.1
+ */
+ String value();
+
+ /**
+ * The token's lifetime, expressed as the number of milliseconds since the
+ * epoch, as per RFC
+ * 6749 Section 1.4
+ *
+ * @return the token'slifetime, expressed as the number of milliseconds since
+ * the epoch, as per
+ * RFC 6749
+ * Section 1.4 .
+ */
+ 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 000000000000..19796e855b08
--- /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 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}.
+ *
+ * Example:
+ * {@code parseMap("key=hey,keyTwo=hi,keyThree=hello", "=", ",") =>
+ * { key: "hey", keyTwo: "hi", keyThree: "hello" }}
+ *
+ */
+ public static Map parseMap(String mapStr,
+ String keyValueSeparator, String elementSeparator) {
+ Map 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 000000000000..2bfd66a7bcaa
--- /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("(?[\\w]+)[ ]+(?[-_\\.a-zA-Z0-9]+)");
+ private static final Pattern CLIENT_INITIAL_RESPONSE_PATTERN = Pattern.compile(
+ String.format("n,(a=(?%s))?,%s(?%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 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 000000000000..39b4330425d9
--- /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 000000000000..86e7d46ea697
--- /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 fd771c722b88..977040eb538b 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 @@
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.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 000000000000..65eafc8edad6
--- /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 = "";
+
+ 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 ac7c4a75e436..fa48fe049b90 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 a40fb964ac79..9d45dfd932c1 100644
--- a/hbase-server/pom.xml
+++ b/hbase-server/pom.xml
@@ -528,6 +528,10 @@
log4j-1.2-api
test
+
+ com.nimbusds
+ nimbus-jose-jwt
+
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 4ebc9fa5325a..bddd3c55edee 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 @@ public void saslReadAndProcess(ByteBuff saslToken) throws IOException,
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 000000000000..ec4b3c2a329e
--- /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
+ * RFC 6749: The OAuth
+ * 2.0 Authorization Framework and the IANA
+ * OAuth Extensions Error Registry . Callback handlers should communicate
+ * other problems by raising an {@code IOException}.
+ *
+ * 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
+ * RFC 7628: A Set
+ * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth
+ * and the IANA
+ * OAuth Extensions Error Registry .
+ *
+ * @return the (potentially null) error status value
+ */
+ public String errorStatus() {
+ return errorStatus;
+ }
+
+ /**
+ * Return the (potentially null) error scope value as per
+ * RFC 7628: A Set
+ * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth .
+ *
+ * @return the (potentially null) error scope value
+ */
+ public String errorScope() {
+ return errorScope;
+ }
+
+ /**
+ * Return the (potentially null) error openid-configuration value as per
+ * RFC 7628: A Set
+ * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth .
+ *
+ * @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
+ * RFC 7628: A Set
+ * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth .
+ * Any token is cleared.
+ *
+ * @param errorStatus
+ * the mandatory error status value from the IANA
+ * OAuth Extensions Error Registry 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 000000000000..3a56d5deb7c7
--- /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
+ *
+ * Note: 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.
+ *
+ */
+ @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 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 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 000000000000..99f7df223b45
--- /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 000000000000..acd5b047e1c8
--- /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 000000000000..09aa28c57ee4
--- /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 000000000000..f17457d66edc
--- /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 claims() {
+ return claims.getClaims();
+ }
+
+ /**
+ * Set required audience, as per
+ *
+ * RFC7519 Section 4.1.3
+ */
+ public OAuthBearerSignedJwt audience(String aud) {
+ this.requiredAudience = aud;
+ return this;
+ }
+
+ /**
+ * Set required issuer, as per
+ *
+ * RFC7519 Section 4.1.1
+ */
+ 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 jwtProcessor = new DefaultJWTProcessor<>();
+
+ Set 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 jwtClaimsSetVerifier =
+ new DefaultJWTClaimsVerifier<>(jwtClaimsSetBuilder.build(), requiredClaims);
+ jwtClaimsSetVerifier.setMaxClockSkew(maxClockSkewSeconds);
+ jwtProcessor.setJWTClaimsSetVerifier(jwtClaimsSetVerifier);
+
+ JWSKeySelector 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 000000000000..8fb33ef5a2f1
--- /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 "exp" (Expiration Time)
+ * claim of type Number. If "iat" (Issued At) or
+ * "nbf" (Not Before) 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:
+ *
+ * {@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.
+ *
+ * {@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.
+ *
+ * {@code hbase.security.oauth.jwt.audience} set to a String value which
+ * you want the desired audience ("aud") the JWT to have.
+ * {@code hbase.security.oauth.jwt.issuer} set to a String value which
+ * you want the issuer ("iss") of the JWT has to be.
+ * {@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)
+ *
+ *
+ * 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 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 000000000000..d41962d5e67b
--- /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 000000000000..b6f8078ccbe8
--- /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 secretManager, Map 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() {
+ @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 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 000000000000..9e6670b19d30
--- /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 000000000000..8be3cfcaf4a4
--- /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 000000000000..377371e7dbcd
--- /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 000000000000..4b72f88bd6c4
--- /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 000000000000..aa6c8e63156e
--- /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 e2a0b621bd5a..bd2c16271dfc 100755
--- a/pom.xml
+++ b/pom.xml
@@ -1821,6 +1821,7 @@
1.9
1.5.0-4
4.0.1
+ 9.15.2
@@ -3414,12 +3415,16 @@
hadoop-distcp
${hadoop-three.version}
-
org.apache.hadoop
hadoop-hdfs-client
${hadoop-three.version}
+
+ com.nimbusds
+ nimbus-jose-jwt
+ ${nimbusds.version}
+