diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java index 4d4559f4b7a9..3238ef187a74 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/ConnectionFactory.java @@ -29,6 +29,7 @@ import org.apache.hadoop.hbase.HBaseConfiguration; 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.trace.TraceUtil; import org.apache.hadoop.hbase.util.FutureUtils; import org.apache.hadoop.hbase.util.ReflectionUtils; @@ -74,6 +75,9 @@ public class ConnectionFactory { public static final String HBASE_CLIENT_ASYNC_CONNECTION_IMPL = "hbase.client.async.connection.impl"; + /** Environment variable for OAuth Bearer token */ + public static final String ENV_OAUTHBEARER_TOKEN = "HBASE_JWT"; + /** No public c.tors */ protected ConnectionFactory() { } @@ -216,6 +220,11 @@ public static Connection createConnection(Configuration conf, User user) throws */ public static Connection createConnection(Configuration conf, ExecutorService pool, final User user) throws IOException { + + if (System.getenv().containsKey(ENV_OAUTHBEARER_TOKEN)) { + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, System.getenv(ENV_OAUTHBEARER_TOKEN)); + } + Class clazz = conf.getClass(ConnectionUtils.HBASE_CLIENT_CONNECTION_IMPL, ConnectionOverAsyncConnection.class, Connection.class); if (clazz != ConnectionOverAsyncConnection.class) { @@ -295,6 +304,12 @@ public static CompletableFuture createAsyncConnection(Configura future.completeExceptionally(new IOException("clusterid came back null")); return; } + + if (System.getenv().containsKey(ENV_OAUTHBEARER_TOKEN)) { + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, + System.getenv(ENV_OAUTHBEARER_TOKEN)); + } + Class clazz = conf.getClass(HBASE_CLIENT_ASYNC_CONNECTION_IMPL, AsyncConnectionImpl.class, AsyncConnection.class); try { 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..6708bef34898 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -0,0 +1,109 @@ +/* + * 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. n * 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..5663e780ac5b --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -0,0 +1,195 @@ +/* + * 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..161c4c380e78 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.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.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..c79fc232edbf --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java @@ -0,0 +1,43 @@ +/* + * 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.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..b2d5be5c04e1 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.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.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 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..93901f81ef3f --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java @@ -0,0 +1,68 @@ +/* + * 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.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..215470f0f319 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java @@ -0,0 +1,124 @@ +/* + * 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 static org.apache.hadoop.hbase.client.ConnectionFactory.ENV_OAUTHBEARER_TOKEN; +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import javax.security.auth.Subject; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils; +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); + + 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); + if (LOG.isDebugEnabled()) { + LOG.debug("JWT token has been added to user credentials with expiry {}", + lifetimeMs == 0 ? "0" : Instant.ofEpochMilli(lifetimeMs).toString()); + } + return null; + } + }); + } + + /** + * Check whether an OAuth Beaerer token is provided in environment variable HBASE_JWT. Parse and + * add it to user private credentials, but only if another token is not already present. + */ + public static void addTokenFromEnvironmentVar(User user, String token) { + Optional> oauthBearerToken = user.getTokens().stream() + .filter((t) -> new Text(OAuthBearerUtils.TOKEN_KIND).equals(t.getKind())).findFirst(); + + if (oauthBearerToken.isPresent()) { + LOG.warn("Ignoring OAuth Bearer token in " + ENV_OAUTHBEARER_TOKEN + " environment " + + "variable, because another token is already present"); + return; + } + + String[] tokens = token.split(","); + if (StringUtils.isEmpty(tokens[0])) { + return; + } + long lifetimeMs = 0; + if (tokens.length > 1) { + try { + ZonedDateTime lifetime = ZonedDateTime.parse(tokens[1]); + lifetimeMs = lifetime.toInstant().toEpochMilli(); + } catch (DateTimeParseException e) { + throw new RuntimeException("Unable to parse JWT expiry: " + tokens[1], e); + } + } else { + throw new RuntimeException("Expiry information of JWT is missing"); + } + + addTokenForUser(user, tokens[0], lifetimeMs); + } +} 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..cad364d5b663 --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java @@ -0,0 +1,77 @@ +/* + * 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..1c4dc4b2b079 --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -0,0 +1,80 @@ +/* + * 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..8c6ec840d69a --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java @@ -0,0 +1,107 @@ +/* + * 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-client/src/test/java/org/apache/hadoop/hbase/security/token/TestOAuthBearerTokenUtil.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/token/TestOAuthBearerTokenUtil.java new file mode 100644 index 000000000000..53281e9281c3 --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/token/TestOAuthBearerTokenUtil.java @@ -0,0 +1,126 @@ +/* + * 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 static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.TOKEN_KIND; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.Instant; +import java.util.Optional; +import java.util.Set; +import javax.security.auth.Subject; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.testclassification.SecurityTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ SecurityTests.class, SmallTests.class }) +public class TestOAuthBearerTokenUtil { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestOAuthBearerTokenUtil.class); + + @Test + public void testAddTokenFromEnvVar() { + // Arrange + User user = User.createUserForTesting(HBaseConfiguration.create(), "testuser", new String[] {}); + String testToken = "some_base64_encoded_stuff,2022-01-25T16:59:48.614000+00:00"; + + // Act + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken); + + // Assert + Optional> oauthBearerToken = + user.getTokens().stream().filter((t) -> new Text(TOKEN_KIND).equals(t.getKind())).findFirst(); + assertTrue("Token cannot be found in user tokens", oauthBearerToken.isPresent()); + user.runAs(new PrivilegedAction() { + @Override + public Object run() { + Subject subject = Subject.getSubject(AccessController.getContext()); + Set tokens = subject.getPrivateCredentials(OAuthBearerToken.class); + assertFalse("Token cannot be found in subject's private credentials", tokens.isEmpty()); + OAuthBearerToken jwt = tokens.iterator().next(); + assertEquals("Invalid encoded JWT value", "some_base64_encoded_stuff", jwt.value()); + assertEquals("Invalid JWT expiry", "2022-01-25T16:59:48.614Z", + Instant.ofEpochMilli(jwt.lifetimeMs()).toString()); + return null; + } + }); + } + + @Test(expected = RuntimeException.class) + public void testAddTokenEnvVarWithoutExpiry() { + // Arrange + User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {}); + String testToken = "some_base64_encoded_stuff"; + + // Act + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken); + + // Assert + } + + @Test(expected = RuntimeException.class) + public void testAddTokenEnvVarWithInvalidExpiry() { + // Arrange + User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {}); + String testToken = "some_base64_encoded_stuff,foobarblahblah328742"; + + // Act + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken); + + // Assert + } + + @Test + public void testAddTokenEnvVarTokenAlreadyPresent() { + // Arrange + User user = User.createUserForTesting(new HBaseConfiguration(), "testuser", new String[] {}); + user.addToken(new Token<>(null, null, new Text(TOKEN_KIND), null)); + String testToken = "some_base64_encoded_stuff,foobarblahblah328742"; + + // Act + OAuthBearerTokenUtil.addTokenFromEnvironmentVar(user, testToken); + + // Assert + long numberOfTokens = + user.getTokens().stream().filter((t) -> new Text(TOKEN_KIND).equals(t.getKind())).count(); + assertEquals("Invalid number of tokens on User", 1, numberOfTokens); + user.runAs(new PrivilegedAction() { + @Override + public Object run() { + Subject subject = Subject.getSubject(AccessController.getContext()); + Set tokens = subject.getPrivateCredentials(OAuthBearerToken.class); + assertTrue("Token should not have been added to subject's credentials", tokens.isEmpty()); + return null; + } + }); + } +} 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..1c20cdbae36c --- /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..b6f1f86cf3d4 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java @@ -0,0 +1,46 @@ +/* + * 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..f5e38dd8eef9 --- /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..f5b5f441ae6c --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.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 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..9ae42188c920 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java @@ -0,0 +1,67 @@ +/* + * 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"; + public static final String TOKEN_KIND = "HBASE_JWT_TOKEN"; + + /** + * 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..d950335c8711 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java @@ -0,0 +1,138 @@ +/* + * 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..db14c4a0ad90 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java @@ -0,0 +1,92 @@ +/* + * 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..7918ff947caf --- /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-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..62c3bb30fc94 --- /dev/null +++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java @@ -0,0 +1,107 @@ +/* + * 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 afbf0b8842c3..24ae2f39cd79 100644 --- a/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm +++ b/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm @@ -1381,7 +1381,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' ]) +#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 f00fba295e2a..983d33d50e25 100644 --- a/hbase-server/pom.xml +++ b/hbase-server/pom.xml @@ -349,6 +349,10 @@ bcpkix-jdk15on test + + com.nimbusds + nimbus-jose-jwt + @@ -1267,6 +1268,11 @@ jettison ${jettison.version} + + com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} + org.slf4j @@ -3952,12 +3958,16 @@ hadoop-distcp ${hadoop-three.version} - org.apache.hadoop hadoop-hdfs-client ${hadoop-three.version} + + com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} +