joshelser commented on a change in pull request #3934:
URL: https://github.com/apache/hbase/pull/3934#discussion_r778396120



##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java
##########
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.exceptions;
+
+import javax.security.sasl.SaslServer;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * This exception indicates that SASL authentication has failed. The error 
message
+ * in the exception indicates the actual cause of failure.
+ * <p>
+ * SASL authentication failures typically indicate invalid credentials, but
+ * could also include other failures specific to the SASL mechanism used
+ * for authentication.
+ * </p>
+ * <p><b>Note:</b>If {@link SaslServer#evaluateResponse(byte[])} throws this 
exception during
+ * authentication, the message from the exception will be sent to clients in 
the SaslAuthenticate
+ * response. Custom {@link SaslServer} implementations may throw this 
exception in order to
+ * provide custom error messages to clients, but should take care not to 
include any
+ * security-critical information in the message that should not be leaked to 
unauthenticated
+ * clients.
+ * </p>
+ */
[email protected]
+public class SaslAuthenticationException extends RuntimeException {

Review comment:
       Same comment here about DoNotRetryIOException, or maybe 
AccessDeniedException (an HBase public exception) is more appropriate if this 
is more for a "bad password" kind of case.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java
##########
@@ -0,0 +1,39 @@
+/*
+ * 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, e.g. if PLAINTEXT protocol
+ * is used to connect to a SASL endpoint.
+ */
[email protected]
+public class IllegalSaslStateException extends IllegalStateException {

Review comment:
       We typically use the `DoNotRetryIOException` through the public API to 
indicate an operation (especially those which might come from a 
Master/RegionServer) should not be intrinsically retried. Since this is a 
RuntimeException, it might be treated as something that is non-retriable 
already.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Optional callback used for SASL mechanisms if any extensions need to be set
+ * in the SASL exchange.
+ */
[email protected]

Review comment:
       Private

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.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.auth;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.yetus.audience.InterfaceAudience;
+
[email protected]

Review comment:
       Private here too

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {
+    return inputExtensions;
+  }
+
+  /**
+   * @return an unmodifiable {@link Map} consisting of the validated and 
recognized by the server
+   *   extension names and values.
+   */
+  public Map<String, String> validatedExtensions() {
+    return Collections.unmodifiableMap(validatedExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the name-&gt;error 
messages of extensions
+   *   which failed validation
+   */
+  public Map<String, String> invalidExtensions() {

Review comment:
       ```suggestion
     public Map<String, String> getInvalidExtensions() {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java
##########
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.oauthbearer;
+
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslClient} and {@code Login}
+ * implementations when they require an OAuth 2 bearer token. Callback handlers
+ * should use the {@link #error(String, String, String)} method to communicate
+ * errors returned by the authorization server as per
+ * <a href="https://tools.ietf.org/html/rfc6749#section-5.2";>RFC 6749: The 
OAuth
+ * 2.0 Authorization Framework</a>. Callback handlers should communicate other
+ * problems by raising an {@code IOException}.
+ * <p>
+ * This class was introduced in 3.0.0 and, while it feels stable, it could
+ * evolve. We will try to evolve the API in a compatible manner, but we reserve
+ * the right to make breaking changes in minor releases, if necessary. We will
+ * update the {@code InterfaceStability} annotation and this notice once the 
API
+ * is considered stable.
+ */
[email protected]
+public class OAuthBearerTokenCallback implements Callback {

Review comment:
       I don't think this would ever have to be public API for HBase. I think 
it can be Private or LimitedPrivate. Really, users would never have to know 
about this class in order to write an HBase application.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java
##########
@@ -0,0 +1,271 @@
+/*
+ * 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.token.OAuthBearerTokenUtil.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.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import 
org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+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).
+ */
[email protected]
+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;
+  private SaslExtensions extensions;
+
+  public OAuthBearerSaslServer(CallbackHandler callbackHandler) {
+    if (!(callbackHandler instanceof AuthenticateCallbackHandler)) {
+      throw new IllegalArgumentException(
+        String.format("Callback handler must be castable to %s: %s",
+          AuthenticateCallbackHandler.class.getName(), 
callbackHandler.getClass().getName()));
+    }
+    this.callbackHandler = (AuthenticateCallbackHandler) callbackHandler;
+  }
+
+  /**
+   * @throws SaslAuthenticationException
+   *             if access token cannot be validated
+   *             <p>
+   *             <b>Note:</b> This method may throw
+   *             {@link SaslAuthenticationException} to provide custom error
+   *             messages to clients. But care should be taken to avoid 
including
+   *             any information in the exception message that should not be
+   *             leaked to unauthenticated clients. It may be safer to throw
+   *             {@link SaslException} in some cases so that a standard error
+   *             message is returned to clients.
+   *             </p>
+   */
+  @Override
+  public byte[] evaluateResponse(byte[] response)
+    throws SaslException, SaslAuthenticationException {
+    try {
+      if (response.length == 1 && response[0] == 
OAuthBearerSaslClient.BYTE_CONTROL_A &&
+        errorMessage != null) {
+        LOG.error("Received %x01 response from client after it received our 
error");
+        throw new SaslAuthenticationException(errorMessage);
+      }
+      errorMessage = null;
+
+      OAuthBearerClientInitialResponse clientResponse;
+      clientResponse = new OAuthBearerClientInitialResponse(response);
+
+      return process(clientResponse.tokenValue(), 
clientResponse.authorizationId(),
+        clientResponse.extensions());
+    } 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();
+    }
+    return extensions.map().get(propName);
+  }
+
+  @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);
+  }

Review comment:
       I thought SASL had some extra header on the data packet which unwrap and 
wrap were handling.  Maybe it's only important when we have SASL QOP set to 
integrity or confidentiality? With the bearer token approach we'll have to make 
sure that the data on the wire is encrypted.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java
##########
@@ -0,0 +1,271 @@
+/*
+ * 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.token.OAuthBearerTokenUtil.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.auth.AuthenticateCallbackHandler;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import 
org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken;
+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).
+ */
[email protected]
+public class OAuthBearerSaslServer implements SaslServer {

Review comment:
       Should move this into hbase-server to keep clients from needing the 
nimbus-jose dependency, I think.

##########
File path: 
hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java
##########
@@ -0,0 +1,98 @@
+/*
+ * 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.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.security.oauthbearer.OAuthBearerToken;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback;
+import org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil;
+import org.junit.Test;
+
+public class OAuthBearerSaslClientCallbackHandlerTest {
+  private static OAuthBearerToken createTokenWithLifetimeMillis(final long 
lifetimeMillis) {
+    return new OAuthBearerToken() {
+      @Override
+      public String value() {
+        return null;
+      }
+
+      @Override
+      public String principalName() {
+        return null;
+      }
+
+      @Override
+      public long lifetimeMs() {
+        return lifetimeMillis;
+      }
+    };
+  }
+
+  @Test
+  public void testWithZeroTokens() {
+    
OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler
 handler =
+      createCallbackHandler();
+    PrivilegedActionException e =
+      assertThrows(PrivilegedActionException.class, () -> Subject.doAs(new 
Subject(),
+      (PrivilegedExceptionAction<Void>) () -> {
+          OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+          handler.handle(new Callback[] {callback});
+          return null;
+        }
+    ));
+    assertEquals(IOException.class, e.getCause().getClass());
+  }
+
+  @Test
+  public void testWithPotentiallyMultipleTokens() throws Exception {
+    
OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler
 handler =
+      createCallbackHandler();
+    Subject.doAs(new Subject(), (PrivilegedExceptionAction<Void>) () -> {
+      final int maxTokens = 4;
+      final Set<Object> privateCredentials = 
Subject.getSubject(AccessController.getContext())
+        .getPrivateCredentials();
+      privateCredentials.clear();
+      for (int num = 1; num <= maxTokens; ++num) {
+        privateCredentials.add(createTokenWithLifetimeMillis(num));
+        OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback();
+        handler.handle(new Callback[] {callback});
+        assertEquals(num, callback.token().lifetimeMs());
+      }

Review comment:
       Would be nice to then add a new `PrivateCredential` with a lesser 
`lifetimeMs`, just to make sure that the sorting is working properly (since you 
test the life times generated in sorted order, `1, 2, 3, 4`).

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {

Review comment:
       ```suggestion
     public OAuthBearerToken getToken() {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {

Review comment:
       ```suggestion
     public SaslExtensions getInputExtensions() {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.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.auth;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.yetus.audience.InterfaceAudience;
+
[email protected]
+public class SaslExtensions {
+  /**
+   * An "empty" instance indicating no SASL extensions
+   */
+  public static final SaslExtensions NO_SASL_EXTENSIONS =
+    new SaslExtensions(Collections.emptyMap());
+  private final Map<String, String> extensionsMap;
+
+  public SaslExtensions(Map<String, String> extensionsMap) {
+    this.extensionsMap = Collections.unmodifiableMap(new 
HashMap<>(extensionsMap));
+  }
+
+  /**
+   * Returns an <strong>immutable</strong> map of the extension names and 
their values
+   */
+  public Map<String, String> map() {

Review comment:
       ```suggestion
     public Map<String, String> getExtensions() {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java
##########
@@ -0,0 +1,39 @@
+/*
+ * 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, e.g. if PLAINTEXT protocol
+ * is used to connect to a SASL endpoint.

Review comment:
       Is the 'PLAINTEXT' comment relevant to HBase, or maybe copied from 
Kafka's implementation?

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java
##########
@@ -0,0 +1,82 @@
+/*
+ * 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 java.util.stream.Collectors;
+import org.apache.yetus.audience.InterfaceAudience;
+
[email protected]
+public final class OAuthBearerStringUtils {
+  /**
+   *  Converts a {@code Map} class into a string, concatenating keys and values
+   *  Example:
+   *      {@code mkString({ key: "hello", keyTwo: "hi" }, "|START|", "|END|", 
"=", ",")
+   *          => "|START|key=hello,keyTwo=hi|END|"}
+   */
+  public static <K, V> String mkString(Map<K, V> map, String begin, String end,

Review comment:
       We have a shaded version of GSON in hbase-thirdparty. That would be 
cleaner than rolling our serialization logic, I think.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {
+    return inputExtensions;
+  }
+
+  /**
+   * @return an unmodifiable {@link Map} consisting of the validated and 
recognized by the server
+   *   extension names and values.
+   */
+  public Map<String, String> validatedExtensions() {
+    return Collections.unmodifiableMap(validatedExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the name-&gt;error 
messages of extensions
+   *   which failed validation
+   */
+  public Map<String, String> invalidExtensions() {
+    return Collections.unmodifiableMap(invalidExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the extensions that have 
neither been
+   *   validated nor invalidated
+   */
+  public Map<String, String> ignoredExtensions() {

Review comment:
       ```suggestion
     public Map<String, String> getIgnoredExtensions() {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {
+    return inputExtensions;
+  }
+
+  /**
+   * @return an unmodifiable {@link Map} consisting of the validated and 
recognized by the server
+   *   extension names and values.
+   */
+  public Map<String, String> validatedExtensions() {
+    return Collections.unmodifiableMap(validatedExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the name-&gt;error 
messages of extensions
+   *   which failed validation
+   */
+  public Map<String, String> invalidExtensions() {
+    return Collections.unmodifiableMap(invalidExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the extensions that have 
neither been
+   *   validated nor invalidated
+   */
+  public Map<String, String> ignoredExtensions() {
+    return Collections.unmodifiableMap(
+      subtractMap(subtractMap(inputExtensions.map(), invalidExtensions), 
validatedExtensions));
+  }
+
+  /**
+   * Validates a specific extension in the original {@code inputExtensions} map
+   * @param extensionName - the name of the extension which was validated
+   */
+  public void valid(String extensionName) {

Review comment:
       ```suggestion
     public void storeAsValid(String extensionName) {
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {
+    return inputExtensions;
+  }
+
+  /**
+   * @return an unmodifiable {@link Map} consisting of the validated and 
recognized by the server
+   *   extension names and values.
+   */
+  public Map<String, String> validatedExtensions() {
+    return Collections.unmodifiableMap(validatedExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the name-&gt;error 
messages of extensions
+   *   which failed validation
+   */
+  public Map<String, String> invalidExtensions() {
+    return Collections.unmodifiableMap(invalidExtensions);
+  }
+
+  /**
+   * @return An immutable {@link Map} consisting of the extensions that have 
neither been
+   *   validated nor invalidated
+   */
+  public Map<String, String> ignoredExtensions() {
+    return Collections.unmodifiableMap(
+      subtractMap(subtractMap(inputExtensions.map(), invalidExtensions), 
validatedExtensions));
+  }
+
+  /**
+   * Validates a specific extension in the original {@code inputExtensions} map
+   * @param extensionName - the name of the extension which was validated
+   */
+  public void valid(String extensionName) {
+    if (!inputExtensions.map().containsKey(extensionName)) {
+      throw new IllegalArgumentException(
+        String.format("Extension %s was not found in the original extensions", 
extensionName));
+    }
+    validatedExtensions.put(extensionName, 
inputExtensions.map().get(extensionName));
+  }
+  /**
+   * Set the error value for a specific extension key-value pair if validation 
has failed
+   *
+   * @param invalidExtensionName
+   *            the mandatory extension name which caused the validation 
failure
+   * @param errorMessage
+   *            error message describing why the validation failed
+   */
+  public void error(String invalidExtensionName, String errorMessage) {

Review comment:
       ```suggestion
     public void storeAsError(String invalidExtensionName, String errorMessage) 
{
   ```

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java
##########
@@ -0,0 +1,125 @@
+/*
+ * 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.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.security.auth.SaslExtensions;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslServer} implementation when it
+ * needs to validate the SASL extensions for the OAUTHBEARER mechanism
+ * Callback handlers should use the {@link #valid(String)}
+ * method to communicate valid extensions back to the SASL server.
+ * Callback handlers should use the
+ * {@link #error(String, String)} method to communicate validation errors back 
to
+ * the SASL Server.
+ * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown 
extensions must be ignored by the server.
+ * The callback handler implementation should simply ignore unknown extensions,
+ * not calling {@link #error(String, String)} nor {@link #valid(String)}.
+ * Callback handlers should communicate other problems by raising an {@code 
IOException}.
+ * <p>
+ * The OAuth bearer token is provided in the callback for better context in 
extension validation.
+ * It is very important that token validation is done in its own
+ * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, 
as they are inherently
+ * insecure.
+ */
[email protected]
+public class OAuthBearerExtensionsValidatorCallback implements Callback {
+  private final OAuthBearerToken token;
+  private final SaslExtensions inputExtensions;
+  private final Map<String, String> validatedExtensions = new HashMap<>();
+  private final Map<String, String> invalidExtensions = new HashMap<>();
+
+  public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, 
SaslExtensions extensions) {
+    this.token = Objects.requireNonNull(token);
+    this.inputExtensions = Objects.requireNonNull(extensions);
+  }
+
+  /**
+   * @return {@link OAuthBearerToken} the OAuth bearer token of the client
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * @return {@link SaslExtensions} consisting of the unvalidated extension 
names and values that
+   *   were sent by the client
+   */
+  public SaslExtensions inputExtensions() {
+    return inputExtensions;
+  }
+
+  /**
+   * @return an unmodifiable {@link Map} consisting of the validated and 
recognized by the server
+   *   extension names and values.
+   */
+  public Map<String, String> validatedExtensions() {

Review comment:
       ```suggestion
     public Map<String, String> getValidatedExtensions() {
   ```

##########
File path: 
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
+ */
[email protected]

Review comment:
       Users would not be implementing this directly, would they? I think good 
to start this off as "Private"

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java
##########
@@ -0,0 +1,212 @@
+/*
+ * 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.auth.SaslExtensions;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * OAuthBearer SASL client's initial message to the server.
+ *
+ * This class has been copy-and-pasted from Kafka codebase.
+ */
[email protected]
+public class OAuthBearerClientInitialResponse {
+  static final String SEPARATOR = "\u0001";
+
+  private static final String SASLNAME = "(?:[\\x01-\\x7F&&[^=,]]|=2C|=3D)+";
+  private static final String KEY = "[A-Za-z]+";
+  private static final String VALUE = "[\\x21-\\x7E \t\r\n]+";
+
+  private static final String KVPAIRS = String.format("(%s=%s%s)*", KEY, 
VALUE, SEPARATOR);
+  private static final Pattern AUTH_PATTERN =
+    Pattern.compile("(?<scheme>[\\w]+)[ ]+(?<token>[-_\\.a-zA-Z0-9]+)");
+  private static final Pattern CLIENT_INITIAL_RESPONSE_PATTERN = 
Pattern.compile(
+    String.format("n,(a=(?<authzid>%s))?,%s(?<kvpairs>%s)%s",
+      SASLNAME, SEPARATOR, KVPAIRS, SEPARATOR));
+  public static final String AUTH_KEY = "auth";
+
+  private final String tokenValue;
+  private final String authorizationId;
+  private final SaslExtensions saslExtensions;
+
+  public static final Pattern EXTENSION_KEY_PATTERN = Pattern.compile(KEY);
+  public static final Pattern EXTENSION_VALUE_PATTERN = Pattern.compile(VALUE);
+
+  public OAuthBearerClientInitialResponse(byte[] response) throws 
SaslException {
+    String responseMsg = new String(response, StandardCharsets.UTF_8);
+    Matcher matcher = CLIENT_INITIAL_RESPONSE_PATTERN.matcher(responseMsg);

Review comment:
       Some `LOG.trace()` calls in here would go a long way to debug why we 
couldn't parse the response.

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java
##########
@@ -0,0 +1,52 @@
+/*
+ * 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.Objects;
+import javax.security.auth.callback.Callback;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Optional callback used for SASL mechanisms if any extensions need to be set
+ * in the SASL exchange.
+ */
[email protected]
+public class SaslExtensionsCallback implements Callback {

Review comment:
       Not clear to me what these "Extensions" are used for. Do we need them 
for the basic functionality?

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java
##########
@@ -0,0 +1,212 @@
+/*
+ * 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.auth.SaslExtensions;
+import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * OAuthBearer SASL client's initial message to the server.
+ *
+ * This class has been copy-and-pasted from Kafka codebase.
+ */
[email protected]
+public class OAuthBearerClientInitialResponse {
+  static final String SEPARATOR = "\u0001";
+
+  private static final String SASLNAME = "(?:[\\x01-\\x7F&&[^=,]]|=2C|=3D)+";
+  private static final String KEY = "[A-Za-z]+";
+  private static final String VALUE = "[\\x21-\\x7E \t\r\n]+";
+
+  private static final String KVPAIRS = String.format("(%s=%s%s)*", KEY, 
VALUE, SEPARATOR);
+  private static final Pattern AUTH_PATTERN =
+    Pattern.compile("(?<scheme>[\\w]+)[ ]+(?<token>[-_\\.a-zA-Z0-9]+)");
+  private static final Pattern CLIENT_INITIAL_RESPONSE_PATTERN = 
Pattern.compile(
+    String.format("n,(a=(?<authzid>%s))?,%s(?<kvpairs>%s)%s",
+      SASLNAME, SEPARATOR, KVPAIRS, SEPARATOR));
+  public static final String AUTH_KEY = "auth";
+
+  private final String tokenValue;
+  private final String authorizationId;
+  private final SaslExtensions saslExtensions;
+
+  public static final Pattern EXTENSION_KEY_PATTERN = Pattern.compile(KEY);
+  public static final Pattern EXTENSION_VALUE_PATTERN = Pattern.compile(VALUE);
+
+  public OAuthBearerClientInitialResponse(byte[] response) throws 
SaslException {
+    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");
+    }
+    String authzid = matcher.group("authzid");
+    this.authorizationId = authzid == null ? "" : authzid;
+    String kvPairs = matcher.group("kvpairs");
+    Map<String, String> properties = OAuthBearerStringUtils.parseMap(kvPairs, 
"=", SEPARATOR);
+    String auth = properties.get(AUTH_KEY);
+    if (auth == null) {
+      throw new SaslException("Invalid OAUTHBEARER client first message: 
'auth' not specified");
+    }
+    properties.remove(AUTH_KEY);
+    SaslExtensions extensions = new SaslExtensions(properties);
+    validateExtensions(extensions);
+    this.saslExtensions = extensions;
+
+    Matcher authMatcher = AUTH_PATTERN.matcher(auth);
+    if (!authMatcher.matches()) {
+      throw new SaslException("Invalid OAUTHBEARER client first message: 
invalid 'auth' format");
+    }
+    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");
+  }
+
+  /**
+   * Constructor
+   *
+   * @param tokenValue
+   *            the mandatory token value
+   * @param extensions
+   *            the optional extensions
+   * @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, SaslExtensions 
extensions)
+    throws SaslException {
+    this(tokenValue, "", extensions);
+  }
+
+  /**
+   * Constructor
+   *
+   * @param tokenValue
+   *            the mandatory token value
+   * @param authorizationId
+   *            the optional authorization ID
+   * @param extensions
+   *            the optional extensions
+   * @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,
+    SaslExtensions extensions) throws SaslException {
+    this.tokenValue = Objects.requireNonNull(tokenValue, "token value must not 
be null");
+    this.authorizationId = authorizationId == null ? "" : authorizationId;
+    validateExtensions(extensions);
+    this.saslExtensions = extensions != null ? extensions : 
SaslExtensions.NO_SASL_EXTENSIONS;
+  }
+
+  /**
+   * Return the always non-null extensions
+   *
+   * @return the always non-null extensions
+   */
+  public SaslExtensions extensions() {
+    return saslExtensions;
+  }
+
+  public byte[] toBytes() {
+    String authzid = authorizationId.isEmpty() ? "" : "a=" + authorizationId;
+    String extensions = extensionsMessage();
+    if (extensions.length() > 0) {
+      extensions = SEPARATOR + extensions;
+    }
+
+    String message = String.format("n,%s,%sauth=Bearer %s%s%s%s", authzid,
+      SEPARATOR, tokenValue, extensions, SEPARATOR, SEPARATOR);
+
+    return message.getBytes(StandardCharsets.UTF_8);

Review comment:
       nit: Could use the helper `Bytes.toBytes(message)` we have in HBase.

##########
File path: hbase-common/pom.xml
##########
@@ -255,6 +255,10 @@
       <artifactId>kerb-simplekdc</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.nimbusds</groupId>
+      <artifactId>nimbus-jose-jwt</artifactId>

Review comment:
       It would be good if we could keep this on the server classpath only. Do 
we need this in hbase-common?

##########
File path: 
hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java
##########
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.security.oauthbearer;
+
+import javax.security.auth.callback.Callback;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * A {@code Callback} for use by the {@code SaslClient} and {@code Login}
+ * implementations when they require an OAuth 2 bearer token. Callback handlers
+ * should use the {@link #error(String, String, String)} method to communicate
+ * errors returned by the authorization server as per
+ * <a href="https://tools.ietf.org/html/rfc6749#section-5.2";>RFC 6749: The 
OAuth
+ * 2.0 Authorization Framework</a>. Callback handlers should communicate other
+ * problems by raising an {@code IOException}.
+ * <p>
+ * This class was introduced in 3.0.0 and, while it feels stable, it could
+ * evolve. We will try to evolve the API in a compatible manner, but we reserve
+ * the right to make breaking changes in minor releases, if necessary. We will
+ * update the {@code InterfaceStability} annotation and this notice once the 
API
+ * is considered stable.
+ */
[email protected]
+public class OAuthBearerTokenCallback implements Callback {
+  private OAuthBearerToken token = null;
+  private String errorCode = null;
+  private String errorDescription = null;
+  private String errorUri = null;
+
+  /**
+   * Return the (potentially null) token
+   *
+   * @return the (potentially null) token
+   */
+  public OAuthBearerToken token() {
+    return token;
+  }
+
+  /**
+   * Return the optional (but always non-empty if not null) error code as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2";>RFC 6749: The 
OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the optional (but always non-empty if not null) error code
+   */
+  public String errorCode() {
+    return errorCode;
+  }
+
+  /**
+   * Return the (potentially null) error description as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2";>RFC 6749: The 
OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the (potentially null) error description
+   */
+  public String errorDescription() {
+    return errorDescription;
+  }
+
+  /**
+   * Return the (potentially null) error URI as per
+   * <a href="https://tools.ietf.org/html/rfc6749#section-5.2";>RFC 6749: The 
OAuth
+   * 2.0 Authorization Framework</a>.
+   *
+   * @return the (potentially null) error URI
+   */
+  public String errorUri() {
+    return errorUri;
+  }
+
+  /**
+   * Set the token. All error-related values are cleared.
+   *
+   * @param token
+   *            the optional token to set
+   */
+  public void token(OAuthBearerToken token) {

Review comment:
       Should these be mutable? For one Callback instance, we would set the 
token the first time and potentially call `error()` later?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to