From 783a05df870db2a639dacfe4d0bf5cedba25b0f6 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Thu, 20 Jan 2022 15:31:30 -0500 Subject: [PATCH] HBASE-26655. Implement new OAuth Bearer SASL plugin Signed-off-by: Josh Elser --- .../oauthbearer/OAuthBearerTokenCallback.java | 122 +++++++++ .../internals/OAuthBearerSaslClient.java | 193 ++++++++++++++ .../OAuthBearerSaslClientProvider.java | 38 +++ ...OAuthBearerSaslAuthenticationProvider.java | 42 ++++ ...earerSaslClientAuthenticationProvider.java | 155 ++++++++++++ .../OAuthBearerSaslProviderSelector.java | 69 +++++ .../security/token/OAuthBearerTokenUtil.java | 75 ++++++ .../OAuthBearerTokenCallbackTest.java | 76 ++++++ .../internals/OAuthBearerSaslClientTest.java | 76 ++++++ ...thBearerSaslClientCallbackHandlerTest.java | 108 ++++++++ .../exceptions/IllegalSaslStateException.java | 35 +++ .../SaslAuthenticationException.java | 47 ++++ .../auth/AuthenticateCallbackHandler.java | 48 ++++ .../oauthbearer/OAuthBearerToken.java | 75 ++++++ .../oauthbearer/OAuthBearerUtils.java | 66 +++++ .../OAuthBearerClientInitialResponse.java | 146 +++++++++++ .../security/oauthbearer/JwtTestUtils.java | 117 +++++++++ .../OAuthBearerClientInitialResponseTest.java | 110 ++++++++ .../hbase/util/ClassLoaderTestHelper.java | 2 - .../jwt/client/example/JwtClientExample.java | 111 ++++++++ .../src/main/resources/META-INF/LICENSE.vm | 2 +- hbase-server/pom.xml | 4 + .../hadoop/hbase/ipc/ServerRpcConnection.java | 2 +- .../OAuthBearerValidatorCallback.java | 156 ++++++++++++ .../internals/OAuthBearerSaslServer.java | 238 ++++++++++++++++++ .../OAuthBearerSaslServerProvider.java | 38 +++ .../knox/OAuthBearerConfigException.java | 37 +++ .../OAuthBearerIllegalTokenException.java | 65 +++++ .../internals/knox/OAuthBearerSignedJwt.java | 193 ++++++++++++++ ...arerSignedJwtValidatorCallbackHandler.java | 196 +++++++++++++++ .../knox/OAuthBearerValidationResult.java | 135 ++++++++++ ...earerSaslServerAuthenticationProvider.java | 100 ++++++++ .../oauthbearer/OAuthBearerTokenMock.java | 34 +++ .../OAuthBearerValidatorCallbackTest.java | 75 ++++++ .../internals/OAuthBearerSaslServerTest.java | 119 +++++++++ .../knox/OAuthBearerSignedJwtTest.java | 121 +++++++++ ...SignedJwtValidatorCallbackHandlerTest.java | 166 ++++++++++++ pom.xml | 7 +- 38 files changed, 3394 insertions(+), 5 deletions(-) create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java create mode 100644 hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java create mode 100644 hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java create mode 100644 hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java create mode 100644 hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java create mode 100644 hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java create mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java new file mode 100644 index 000000000000..a9e28efbc2a8 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import javax.security.auth.callback.Callback; +import org.apache.commons.lang3.StringUtils; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * A {@code Callback} for use by the {@code SaslClient} and {@code Login} + * implementations when they require an OAuth 2 bearer token. Callback handlers + * should use the {@link #error(String, String, String)} method to communicate + * errors returned by the authorization server as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework. Callback handlers should communicate other + * problems by raising an {@code IOException}. + *

+ * This class was introduced in 3.0.0 and, while it feels stable, it could + * evolve. We will try to evolve the API in a compatible manner, but we reserve + * the right to make breaking changes in minor releases, if necessary. We will + * update the {@code InterfaceStability} annotation and this notice once the API + * is considered stable. + */ +@InterfaceAudience.Private +public class OAuthBearerTokenCallback implements Callback { + private OAuthBearerToken token = null; + private String errorCode = null; + private String errorDescription = null; + private String errorUri = null; + + /** + * Return the (potentially null) token + * + * @return the (potentially null) token + */ + public OAuthBearerToken token() { + return token; + } + + /** + * Return the optional (but always non-empty if not null) error code as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework. + * + * @return the optional (but always non-empty if not null) error code + */ + public String errorCode() { + return errorCode; + } + + /** + * Return the (potentially null) error description as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework. + * + * @return the (potentially null) error description + */ + public String errorDescription() { + return errorDescription; + } + + /** + * Return the (potentially null) error URI as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework. + * + * @return the (potentially null) error URI + */ + public String errorUri() { + return errorUri; + } + + /** + * Set the token. All error-related values are cleared. + * + * @param token + * the optional token to set + */ + public void token(OAuthBearerToken token) { + this.token = token; + this.errorCode = null; + this.errorDescription = null; + this.errorUri = null; + } + + /** + * Set the error values as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework. Any token is cleared. + * + * @param errorCode + * the mandatory error code to set + * @param errorDescription + * the optional error description to set + * @param errorUri + * the optional error URI to set + */ + public void error(String errorCode, String errorDescription, String errorUri) { + if (StringUtils.isEmpty(errorCode)) { + throw new IllegalArgumentException("error code must not be empty"); + } + this.errorCode = errorCode; + this.errorDescription = errorDescription; + this.errorUri = errorUri; + this.token = null; + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java new file mode 100644 index 000000000000..2fa07b5a5110 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslClientFactory; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.exceptions.IllegalSaslStateException; +import org.apache.hadoop.hbase.security.SaslUtil; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code SaslClient} implementation for SASL/OAUTHBEARER in Kafka. This + * implementation requires an instance of {@code AuthenticateCallbackHandler} + * that can handle an instance of {@link OAuthBearerTokenCallback} and return + * the {@link OAuthBearerToken} generated by the {@code login()} event on the + * {@code LoginContext}. + * + * See RFC 6750 Section 2.1 + * + * This class has been copy-and-pasted from Kafka codebase. + */ +@InterfaceAudience.Public +public class OAuthBearerSaslClient implements SaslClient { + static final byte BYTE_CONTROL_A = (byte) 0x01; + private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslClient.class); + private final CallbackHandler callbackHandler; + + enum State { + SEND_CLIENT_FIRST_MESSAGE, RECEIVE_SERVER_FIRST_MESSAGE, RECEIVE_SERVER_MESSAGE_AFTER_FAILURE, + COMPLETE, FAILED + } + + private State state; + + public OAuthBearerSaslClient(AuthenticateCallbackHandler callbackHandler) { + this.callbackHandler = Objects.requireNonNull(callbackHandler); + setState(State.SEND_CLIENT_FIRST_MESSAGE); + } + + public CallbackHandler callbackHandler() { + return callbackHandler; + } + + @Override + public String getMechanismName() { + return OAUTHBEARER_MECHANISM; + } + + @Override + public boolean hasInitialResponse() { + return true; + } + + @Override + public byte[] evaluateChallenge(byte[] challenge) throws SaslException { + try { + OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback(); + switch (state) { + case SEND_CLIENT_FIRST_MESSAGE: + if (challenge != null && challenge.length != 0) { + throw new SaslException("Expected empty challenge"); + } + callbackHandler().handle(new Callback[] {callback}); + setState(State.RECEIVE_SERVER_FIRST_MESSAGE); + return new OAuthBearerClientInitialResponse(callback.token().value()).toBytes(); + case RECEIVE_SERVER_FIRST_MESSAGE: + if (challenge != null && challenge.length != 0) { + String jsonErrorResponse = new String(challenge, StandardCharsets.UTF_8); + if (LOG.isDebugEnabled()) { + LOG.debug("Sending %%x01 response to server after receiving an error: {}", + jsonErrorResponse); + } + setState(State.RECEIVE_SERVER_MESSAGE_AFTER_FAILURE); + return new byte[] {BYTE_CONTROL_A}; + } + callbackHandler().handle(new Callback[] {callback}); + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully authenticated as {}", callback.token().principalName()); + } + setState(State.COMPLETE); + return null; + default: + throw new IllegalSaslStateException("Unexpected challenge in Sasl client state " + state); + } + } catch (SaslException e) { + setState(State.FAILED); + throw e; + } catch (IOException | UnsupportedCallbackException e) { + setState(State.FAILED); + throw new SaslException(e.getMessage(), e); + } + } + + @Override + public boolean isComplete() { + return state == State.COMPLETE; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) { + if (!isComplete()) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(incoming, offset, offset + len); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) { + if (!isComplete()) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(outgoing, offset, offset + len); + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!isComplete()) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + if (Sasl.QOP.equals(propName)) { + return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop(); + } + return null; + } + + @Override + public void dispose() { + } + + private void setState(State state) { + LOG.debug("Setting SASL/{} client state to {}", OAUTHBEARER_MECHANISM, state); + this.state = state; + } + + public static class OAuthBearerSaslClientFactory implements SaslClientFactory { + @Override + public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol, + String serverName, Map props, CallbackHandler callbackHandler) { + String[] mechanismNamesCompatibleWithPolicy = getMechanismNames(props); + for (String mechanism : mechanisms) { + for (String s : mechanismNamesCompatibleWithPolicy) { + if (s.equals(mechanism)) { + if (!(Objects.requireNonNull(callbackHandler) instanceof AuthenticateCallbackHandler)) { + throw new IllegalArgumentException( + String.format("Callback handler must be castable to %s: %s", + AuthenticateCallbackHandler.class.getName(), + callbackHandler.getClass().getName())); + } + return new OAuthBearerSaslClient((AuthenticateCallbackHandler) callbackHandler); + } + } + } + return null; + } + + @Override + public String[] getMechanismNames(Map props) { + return OAuthBearerUtils.mechanismNamesCompatibleWithPolicy(props); + } + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java new file mode 100644 index 000000000000..3ba721779e99 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import java.security.Provider; +import java.security.Security; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public class OAuthBearerSaslClientProvider extends Provider { + private static final long serialVersionUID = 1L; + + protected OAuthBearerSaslClientProvider() { + super("SASL/OAUTHBEARER Client Provider", 1.0, "SASL/OAUTHBEARER Client Provider for HBase"); + put("SaslClientFactory." + OAUTHBEARER_MECHANISM, + OAuthBearerSaslClient.OAuthBearerSaslClientFactory.class.getName()); + } + + public static void initialize() { + Security.addProvider(new OAuthBearerSaslClientProvider()); + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java new file mode 100644 index 000000000000..8b4dcfe5c75b --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslAuthenticationProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Base client for client/server implementations for the OAuth Bearer (JWT) token auth'n method. + */ +@InterfaceAudience.Private +public class OAuthBearerSaslAuthenticationProvider extends BuiltInSaslAuthenticationProvider { + + public static final SaslAuthMethod SASL_AUTH_METHOD = new SaslAuthMethod( + "OAUTHBEARER", (byte)83, "OAUTHBEARER", UserGroupInformation.AuthenticationMethod.TOKEN); + + @Override + public SaslAuthMethod getSaslAuthMethod() { + return SASL_AUTH_METHOD; + } + + @Override + public String getTokenKind() { + return TOKEN_KIND; + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java new file mode 100644 index 000000000000..915706c1027b --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import java.io.IOException; +import java.net.InetAddress; +import java.security.AccessController; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.TreeSet; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.SaslUtil; +import org.apache.hadoop.hbase.security.SecurityInfo; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.hadoop.hbase.shaded.protobuf.generated.RPCProtos; + +@InterfaceAudience.Private +public class OAuthBearerSaslClientAuthenticationProvider + extends OAuthBearerSaslAuthenticationProvider + implements SaslClientAuthenticationProvider { + + @Override + public SaslClient createClient(Configuration conf, InetAddress serverAddr, + SecurityInfo securityInfo, Token token, + boolean fallbackAllowed, + Map saslProps) throws IOException { + AuthenticateCallbackHandler callbackHandler = new OAuthBearerSaslClientCallbackHandler(); + callbackHandler.configure(conf, getSaslAuthMethod().getSaslMechanism(), saslProps); + return Sasl.createSaslClient(new String[] { getSaslAuthMethod().getSaslMechanism() }, null, + null, SaslUtil.SASL_DEFAULT_REALM, saslProps, callbackHandler); + } + + public static class OAuthBearerSaslClientCallbackHandler implements AuthenticateCallbackHandler { + private static final Logger LOG = + LoggerFactory.getLogger(OAuthBearerSaslClientCallbackHandler.class); + private boolean configured = false; + + @Override public void configure(Configuration configs, String saslMechanism, + Map saslProps) { + if (!OAUTHBEARER_MECHANISM.equals(saslMechanism)) { + throw new IllegalArgumentException( + String.format("Unexpected SASL mechanism: %s", saslMechanism)); + } + this.configured = true; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + if (!configured) { + throw new IllegalStateException( + "OAuthBearerSaslClientCallbackHandler handler must be configured first."); + } + + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerTokenCallback) { + handleCallback((OAuthBearerTokenCallback) callback); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + private void handleCallback(OAuthBearerTokenCallback callback) throws IOException { + if (callback.token() != null) { + throw new IllegalArgumentException("Callback had a token already"); + } + Subject subject = Subject.getSubject(AccessController.getContext()); + Set privateCredentials = subject != null + ? subject.getPrivateCredentials(OAuthBearerToken.class) + : Collections.emptySet(); + callback.token(choosePrivateCredential(privateCredentials)); + } + + private OAuthBearerToken choosePrivateCredential(Set privateCredentials) + throws IOException { + if (privateCredentials.size() == 0) { + throw new IOException("No OAuth Bearer tokens in Subject's private credentials"); + } + if (privateCredentials.size() == 1) { + LOG.debug("Found 1 OAuthBearer token"); + return privateCredentials.iterator().next(); + } else { + /* + * There a very small window of time upon token refresh (on the order of milliseconds) + * where both an old and a new token appear on the Subject's private credentials. + * Rather than implement a lock to eliminate this window, we will deal with it by + * checking for the existence of multiple tokens and choosing the one that has the + * longest lifetime. It is also possible that a bug could cause multiple tokens to + * exist (e.g. KAFKA-7902), so dealing with the unlikely possibility that occurs + * during normal operation also allows us to deal more robustly with potential bugs. + */ + NavigableSet sortedByLifetime = + new TreeSet<>( + new Comparator() { + @Override + public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { + return Long.compare(o1.lifetimeMs(), o2.lifetimeMs()); + } + }); + sortedByLifetime.addAll(privateCredentials); + if (LOG.isWarnEnabled()) { + LOG.warn("Found {} OAuth Bearer tokens in Subject's private credentials; " + + "the oldest expires at {}, will use the newest, which expires at {}", + sortedByLifetime.size(), + LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.first().lifetimeMs()), + ZoneId.systemDefault()), + LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.last().lifetimeMs()), + ZoneId.systemDefault())); + } + return sortedByLifetime.last(); + } + } + } + + @Override + public RPCProtos.UserInformation getUserInfo(User user) { + // Don't send user for token auth. Copied from RpcConnection. + return null; + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java new file mode 100644 index 000000000000..88c2eed0c953 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslProviderSelector.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.TOKEN_KIND; +import java.util.Collection; +import java.util.Optional; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.util.Pair; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@InterfaceAudience.Private +public class OAuthBearerSaslProviderSelector extends BuiltInProviderSelector { + + private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslProviderSelector.class); + + private final Text OAUTHBEARER_TOKEN_KIND_TEXT = + new Text(TOKEN_KIND); + private OAuthBearerSaslClientAuthenticationProvider oauthbearer; + + @Override public void configure(Configuration conf, + Collection providers) { + super.configure(conf, providers); + + this.oauthbearer = (OAuthBearerSaslClientAuthenticationProvider) providers.stream() + .filter((p) -> p instanceof OAuthBearerSaslClientAuthenticationProvider) + .findFirst() + .orElseThrow(() -> new RuntimeException( + "OAuthBearerSaslClientAuthenticationProvider not loaded")); + } + + @Override + public Pair> selectProvider( + String clusterId, User user) { + Pair> pair = + super.selectProvider(clusterId, user); + + Optional> optional = user.getTokens().stream() + .filter((t) -> OAUTHBEARER_TOKEN_KIND_TEXT.equals(t.getKind())) + .findFirst(); + if (optional.isPresent()) { + LOG.info("OAuthBearer token found in user tokens"); + return new Pair<>(oauthbearer, optional.get()); + } + + return pair; + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java new file mode 100644 index 000000000000..d9e42d5973b4 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java @@ -0,0 +1,75 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.token; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import javax.security.auth.Subject; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslClientProvider; +import org.apache.hadoop.io.Text; +import org.apache.hadoop.security.token.Token; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility methods for obtaining OAuthBearer / JWT authentication tokens. + */ +@InterfaceAudience.Public +public final class OAuthBearerTokenUtil { + private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerTokenUtil.class); + public static final String TOKEN_KIND = "JWT_AUTH_TOKEN"; + + static { + OAuthBearerSaslClientProvider.initialize(); // not part of public API + LOG.info("OAuthBearer SASL client provider has been initialized"); + } + + private OAuthBearerTokenUtil() { } + + /** + * Add token to user's subject private credentials and a hint to provider selector + * to correctly select OAuthBearer SASL provider. + */ + public static void addTokenForUser(User user, String encodedToken, long lifetimeMs) { + user.addToken(new Token<>(null, null, new Text(TOKEN_KIND), null)); + user.runAs(new PrivilegedAction() { + @Override public Object run() { + Subject subject = Subject.getSubject(AccessController.getContext()); + OAuthBearerToken jwt = new OAuthBearerToken() { + @Override public String value() { + return encodedToken; + } + + @Override public long lifetimeMs() { + return lifetimeMs; + } + + @Override public String principalName() { + return user.getName(); + } + }; + subject.getPrivateCredentials().add(jwt); + return null; + } + }); + } +} diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java new file mode 100644 index 000000000000..06ce6577ce5d --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerTokenCallbackTest { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerTokenCallbackTest.class); + + private static final OAuthBearerToken TOKEN = new OAuthBearerToken() { + @Override + public String value() { + return "value"; + } + + @Override + public String principalName() { + return "principalName"; + } + + @Override + public long lifetimeMs() { + return 0; + } + }; + + @Test + public void testError() { + String errorCode = "errorCode"; + String errorDescription = "errorDescription"; + String errorUri = "errorUri"; + OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback(); + callback.error(errorCode, errorDescription, errorUri); + assertEquals(errorCode, callback.errorCode()); + assertEquals(errorDescription, callback.errorDescription()); + assertEquals(errorUri, callback.errorUri()); + assertNull(callback.token()); + } + + @Test + public void testToken() { + OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback(); + callback.token(TOKEN); + assertSame(TOKEN, callback.token()); + assertNull(callback.errorCode()); + assertNull(callback.errorDescription()); + assertNull(callback.errorUri()); + } +} diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java new file mode 100644 index 000000000000..0267084d35e7 --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.junit.Assert.assertEquals; +import java.nio.charset.StandardCharsets; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerSaslClientTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerSaslClientTest.class); + + public static class ExtensionsCallbackHandler implements AuthenticateCallbackHandler { + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerTokenCallback) { + ((OAuthBearerTokenCallback) callback).token(new OAuthBearerToken() { + @Override public String value() { + return ""; + } + + @Override public long lifetimeMs() { + return 100; + } + + @Override public String principalName() { + return "principalName"; + } + }); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + } + + @Test + public void testAttachesExtensionsToFirstClientMessage() throws Exception { + String expectedToken = new String( + new OAuthBearerClientInitialResponse("").toBytes(), StandardCharsets.UTF_8); + OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler()); + String message = new String(client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); + assertEquals(expectedToken, message); + } + +} diff --git a/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java new file mode 100644 index 000000000000..caf85db44faa --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.Set; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerSaslClientCallbackHandlerTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerSaslClientCallbackHandlerTest.class); + + private static OAuthBearerToken createTokenWithLifetimeMillis(final long lifetimeMillis) { + return new OAuthBearerToken() { + @Override + public String value() { + return null; + } + + @Override + public String principalName() { + return null; + } + + @Override + public long lifetimeMs() { + return lifetimeMillis; + } + }; + } + + @Test + public void testWithZeroTokens() { + OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = + createCallbackHandler(); + PrivilegedActionException e = + assertThrows(PrivilegedActionException.class, () -> Subject.doAs(new Subject(), + (PrivilegedExceptionAction) () -> { + OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback(); + handler.handle(new Callback[] {callback}); + return null; + } + )); + assertEquals(IOException.class, e.getCause().getClass()); + } + + @Test + public void testWithPotentiallyMultipleTokens() throws Exception { + OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = + createCallbackHandler(); + Subject.doAs(new Subject(), (PrivilegedExceptionAction) () -> { + final int maxTokens = 4; + final Set privateCredentials = Subject.getSubject(AccessController.getContext()) + .getPrivateCredentials(); + privateCredentials.clear(); + for (int num = 1; num <= maxTokens; ++num) { + privateCredentials.add(createTokenWithLifetimeMillis(num)); + privateCredentials.add(createTokenWithLifetimeMillis(-num)); + OAuthBearerTokenCallback callback = new OAuthBearerTokenCallback(); + handler.handle(new Callback[] {callback}); + assertEquals(num, callback.token().lifetimeMs()); + } + return null; + }); + } + + private static OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler + createCallbackHandler() { + OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = + new OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler(); + handler.configure(new Configuration(), OAUTHBEARER_MECHANISM, Collections.emptyMap()); + return handler; + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java new file mode 100644 index 000000000000..ce7d1f7c3ddb --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.exceptions; + +import org.apache.yetus.audience.InterfaceAudience; + +/** + * This exception indicates unexpected requests prior to SASL authentication. + * This could be due to misconfigured security. + */ +@InterfaceAudience.Public +public class IllegalSaslStateException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + public IllegalSaslStateException(String message) { + super(message); + } + +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java new file mode 100644 index 000000000000..3f4866e0f557 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.exceptions; + +import javax.security.sasl.SaslServer; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * This exception indicates that SASL authentication has failed. The error message + * in the exception indicates the actual cause of failure. + *

+ * SASL authentication failures typically indicate invalid credentials, but + * could also include other failures specific to the SASL mechanism used + * for authentication. + *

+ *

Note:If {@link SaslServer#evaluateResponse(byte[])} throws this exception during + * authentication, the message from the exception will be sent to clients in the SaslAuthenticate + * response. Custom {@link SaslServer} implementations may throw this exception in order to + * provide custom error messages to clients, but should take care not to include any + * security-critical information in the message that should not be leaked to unauthenticated + * clients. + *

+ */ +@InterfaceAudience.Public +public class SaslAuthenticationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public SaslAuthenticationException(String message) { + super(message); + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java new file mode 100644 index 000000000000..1329e9ac67f7 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.auth; + +import java.util.Map; +import javax.security.auth.callback.CallbackHandler; +import org.apache.hadoop.conf.Configuration; +import org.apache.yetus.audience.InterfaceAudience; + +/* + * Callback handler for SASL-based authentication + */ +@InterfaceAudience.Private +public interface AuthenticateCallbackHandler extends CallbackHandler { + + /** + * Configures this callback handler for the specified SASL mechanism. + * + * @param configs Key-value pairs containing the parsed configuration options of + * the client or server. Note that these are the HBase configuration options + * and not the JAAS configuration options. JAAS config options may be obtained + * from `jaasConfigEntries` for callbacks which obtain some configs from the + * JAAS configuration. For configs that may be specified as both HBase config + * as well as JAAS config (e.g. sasl.kerberos.service.name), the configuration + * is treated as invalid if conflicting values are provided. + * @param saslMechanism Negotiated SASL mechanism. For clients, this is the SASL + * mechanism configured for the client. For brokers, this is the mechanism + * negotiated with the client and is one of the mechanisms enabled on the broker. + * @param saslProps SASL properties provided by the SASL library. + */ + default void configure( + Configuration configs, String saslMechanism, Map saslProps) {} +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java new file mode 100644 index 000000000000..769bceea6181 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerToken.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; + +/** + * The b64token value as defined in + * RFC 6750 Section + * 2.1 along with the token's specific scope and lifetime and principal + * name. + *

+ * A network request would be required to re-hydrate an opaque token, and that + * could result in (for example) an {@code IOException}, but retrievers for + * various attributes ({@link #lifetimeMs()}, etc.) declare no + * exceptions. Therefore, if a network request is required for any of these + * retriever methods, that request could be performed at construction time so + * that the various attributes can be reliably provided thereafter. For example, + * a constructor might declare {@code throws IOException} in such a case. + * Alternatively, the retrievers could throw unchecked exceptions. + * + * @see RFC 6749 + * Section 1.4 and + * RFC 6750 + * Section 2.1 + */ +@InterfaceAudience.Public +@InterfaceStability.Evolving +public interface OAuthBearerToken { + /** + * The b64token value as defined in + * RFC 6750 Section + * 2.1 + * + * @return b64token value as defined in + * RFC 6750 + * Section 2.1 + */ + String value(); + + /** + * The token's lifetime, expressed as the number of milliseconds since the + * epoch, as per RFC + * 6749 Section 1.4 + * + * @return the token'slifetime, expressed as the number of milliseconds since + * the epoch, as per + * RFC 6749 + * Section 1.4. + */ + long lifetimeMs(); + + /** + * The name of the principal to which this credential applies + * + * @return the always non-null/non-empty principal name + */ + String principalName(); +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java new file mode 100644 index 000000000000..19796e855b08 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import java.util.HashMap; +import java.util.Map; +import javax.security.sasl.Sasl; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Private +public final class OAuthBearerUtils { + public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER"; + + /** + * Verifies configuration for OAuth Bearer authentication mechanism. + * Throws RuntimeException if PlainText is not allowed. + */ + public static String[] mechanismNamesCompatibleWithPolicy(Map props) { + if (props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT)))) { + throw new RuntimeException("OAuth Bearer authentication mech cannot be used if plaintext is " + + "disallowed."); + } + return new String[] { OAUTHBEARER_MECHANISM }; + } + + /** + * Converts an extensions string into a {@code Map}. + * + * Example: + * {@code parseMap("key=hey,keyTwo=hi,keyThree=hello", "=", ",") => + * { key: "hey", keyTwo: "hi", keyThree: "hello" }} + * + */ + public static Map parseMap(String mapStr, + String keyValueSeparator, String elementSeparator) { + Map map = new HashMap<>(); + + if (!mapStr.isEmpty()) { + String[] attrvals = mapStr.split(elementSeparator); + for (String attrval : attrvals) { + String[] array = attrval.split(keyValueSeparator, 2); + map.put(array[0], array[1]); + } + } + return map; + } + + private OAuthBearerUtils() { + // empty + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java new file mode 100644 index 000000000000..2bfd66a7bcaa --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OAuthBearer SASL client's initial message to the server. + * + * This class has been copy-and-pasted from Kafka codebase. + */ +@InterfaceAudience.Public +public class OAuthBearerClientInitialResponse { + private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerClientInitialResponse.class); + static final String SEPARATOR = "\u0001"; + + private static final String SASLNAME = "(?:[\\x01-\\x7F&&[^=,]]|=2C|=3D)+"; + private static final String KEY = "[A-Za-z]+"; + private static final String VALUE = "[\\x21-\\x7E \t\r\n]+"; + + private static final String KVPAIRS = String.format("(%s=%s%s)*", KEY, VALUE, SEPARATOR); + private static final Pattern AUTH_PATTERN = + Pattern.compile("(?[\\w]+)[ ]+(?[-_\\.a-zA-Z0-9]+)"); + private static final Pattern CLIENT_INITIAL_RESPONSE_PATTERN = Pattern.compile( + String.format("n,(a=(?%s))?,%s(?%s)%s", + SASLNAME, SEPARATOR, KVPAIRS, SEPARATOR)); + public static final String AUTH_KEY = "auth"; + + private final String tokenValue; + private final String authorizationId; + + public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { + LOG.trace("Client initial response parsing started"); + String responseMsg = new String(response, StandardCharsets.UTF_8); + Matcher matcher = CLIENT_INITIAL_RESPONSE_PATTERN.matcher(responseMsg); + if (!matcher.matches()) { + throw new SaslException("Invalid OAUTHBEARER client first message"); + } + LOG.trace("Client initial response matches pattern"); + String authzid = matcher.group("authzid"); + this.authorizationId = authzid == null ? "" : authzid; + String kvPairs = matcher.group("kvpairs"); + Map properties = OAuthBearerUtils.parseMap(kvPairs, "=", SEPARATOR); + String auth = properties.get(AUTH_KEY); + if (auth == null) { + throw new SaslException("Invalid OAUTHBEARER client first message: 'auth' not specified"); + } + LOG.trace("Auth key found in client initial response"); + properties.remove(AUTH_KEY); + Matcher authMatcher = AUTH_PATTERN.matcher(auth); + if (!authMatcher.matches()) { + throw new SaslException("Invalid OAUTHBEARER client first message: invalid 'auth' format"); + } + LOG.trace("Client initial response auth matches pattern"); + if (!"bearer".equalsIgnoreCase(authMatcher.group("scheme"))) { + String msg = String.format("Invalid scheme in OAUTHBEARER client first message: %s", + matcher.group("scheme")); + throw new SaslException(msg); + } + this.tokenValue = authMatcher.group("token"); + LOG.trace("Client initial response parsing finished"); + } + + /** + * Constructor + * + * @param tokenValue + * the mandatory token value + * @throws SaslException + * if any extension name or value fails to conform to the required + * regular expression as defined by the specification, or if the + * reserved {@code auth} appears as a key + */ + public OAuthBearerClientInitialResponse(String tokenValue) { + this(tokenValue, ""); + } + + /** + * Constructor + * + * @param tokenValue + * the mandatory token value + * @param authorizationId + * the optional authorization ID + * @throws SaslException + * if any extension name or value fails to conform to the required + * regular expression as defined by the specification, or if the + * reserved {@code auth} appears as a key + */ + public OAuthBearerClientInitialResponse(String tokenValue, String authorizationId) { + this.tokenValue = Objects.requireNonNull(tokenValue, "token value must not be null"); + this.authorizationId = authorizationId == null ? "" : authorizationId; + } + + public byte[] toBytes() { + String authzid = authorizationId.isEmpty() ? "" : "a=" + authorizationId; + + String message = String.format("n,%s,%sauth=Bearer %s%s%s", authzid, + SEPARATOR, tokenValue, SEPARATOR, SEPARATOR); + + return Bytes.toBytes(message); + } + + /** + * Return the always non-null token value + * + * @return the always non-null toklen value + */ + public String tokenValue() { + return tokenValue; + } + + /** + * Return the always non-null authorization ID + * + * @return the always non-null authorization ID + */ + public String authorizationId() { + return authorizationId; + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java new file mode 100644 index 000000000000..39b4330425d9 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public final class JwtTestUtils { + private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles"); + public static final String USER = "user"; + + public static RSAKey generateRSAKey() throws JOSEException { + RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048); + return rsaKeyGenerator.keyID("1").generate(); + } + + public static String createSignedJwt(RSAKey rsaKey, String issuer, String subject, + LocalDate expirationTime, LocalDate issueTime, String audience) + throws JOSEException { + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .issuer(issuer) + .subject(subject) + .issueTime(java.sql.Date.valueOf(issueTime)) + .expirationTime(java.sql.Date.valueOf(expirationTime)) + .audience(audience) + .build(); + SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); + signedJwt.sign(new RSASSASigner(rsaKey)); + return signedJwt.serialize(); + } + + public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { + LocalDateTime now = LocalDateTime.now(ZONE_ID); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) + .build(); + SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); + signedJwt.sign(new RSASSASigner(rsaKey)); + return signedJwt.serialize(); + } + + public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) throws JOSEException { + LocalDateTime now = LocalDateTime.now(ZONE_ID); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) + .audience(aud) + .build(); + SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); + signedJwt.sign(new RSASSASigner(rsaKey)); + return signedJwt.serialize(); + } + + public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws JOSEException { + LocalDateTime now = LocalDateTime.now(ZONE_ID); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) + .issuer(iss) + .build(); + SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); + signedJwt.sign(new RSASSASigner(rsaKey)); + return signedJwt.serialize(); + } + + private JwtTestUtils() { + // empty + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java new file mode 100644 index 000000000000..86e7d46ea697 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.junit.Assert.assertEquals; +import java.nio.charset.StandardCharsets; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerClientInitialResponseTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerClientInitialResponseTest.class); + + /* + Test how a client would build a response + */ + @Test + public void testBuildClientResponseToBytes() { + String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; + + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse("123.345.567"); + + String message = new String(response.toBytes(), StandardCharsets.UTF_8); + + assertEquals(expectedMesssage, message); + } + + @Test + public void testBuildServerResponseToBytes() throws Exception { + String serverMessage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(serverMessage.getBytes(StandardCharsets.UTF_8)); + + String message = new String(response.toBytes(), StandardCharsets.UTF_8); + + assertEquals(serverMessage, message); + } + + @Test + public void testToken() throws Exception { + String message = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); + assertEquals("123.345.567", response.tokenValue()); + assertEquals("", response.authorizationId()); + } + + @Test + public void testAuthorizationId() throws Exception { + String message = "n,a=myuser,\u0001auth=Bearer 345\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); + assertEquals("345", response.tokenValue()); + assertEquals("myuser", response.authorizationId()); + } + + @Test + public void testExtensions() throws Exception { + String message = + "n,,\u0001propA=valueA1, valueA2\u0001auth=Bearer 567\u0001propB=valueB\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); + assertEquals("567", response.tokenValue()); + assertEquals("", response.authorizationId()); + } + + // The example in the RFC uses `vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==` as the token + // But since we use Base64Url encoding, padding is omitted. Hence this test verifies without '='. + @Test + public void testRfc7688Example() throws Exception { + String message = "n,a=user@example.com,\u0001host=server.example.com\u0001port=143\u0001" + + "auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); + assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); + assertEquals("user@example.com", response.authorizationId()); + } + + @Test + public void testNoExtensionsFromByteArray() throws Exception { + String message = "n,a=user@example.com,\u0001" + + "auth=Bearer vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg\u0001\u0001"; + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); + assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); + assertEquals("user@example.com", response.authorizationId()); + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java index decc0da2e9b4..094ffe6936d2 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; - import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; @@ -35,7 +34,6 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.slf4j.Logger; diff --git a/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java new file mode 100644 index 000000000000..65eafc8edad6 --- /dev/null +++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java @@ -0,0 +1,111 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.jwt.client.example; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadLocalRandom; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.conf.Configured; +import org.apache.hadoop.hbase.Cell; +import org.apache.hadoop.hbase.CellBuilderFactory; +import org.apache.hadoop.hbase.CellBuilderType; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Admin; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; +import org.apache.hadoop.hbase.client.Connection; +import org.apache.hadoop.hbase.client.ConnectionFactory; +import org.apache.hadoop.hbase.client.Put; +import org.apache.hadoop.hbase.client.Table; +import org.apache.hadoop.hbase.client.TableDescriptor; +import org.apache.hadoop.hbase.client.TableDescriptorBuilder; +import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.UserProvider; +import org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.util.Tool; +import org.apache.hadoop.util.ToolRunner; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An example of using OAuthBearer (JWT) authentication with HBase RPC client. + */ +@InterfaceAudience.Private +public class JwtClientExample extends Configured implements Tool { + private static final Logger LOG = LoggerFactory.getLogger(JwtClientExample.class); + private static final String JWT_TOKEN = ""; + + private static final byte[] FAMILY = Bytes.toBytes("d"); + + public JwtClientExample() { + Configuration conf = HBaseConfiguration.create(); + conf.set("hbase.client.sasl.provider.class", + "org.apache.hadoop.hbase.security.provider.OAuthBearerSaslProviderSelector"); + conf.set("hbase.client.sasl.provider.extras", + "org.apache.hadoop.hbase.security.provider.OAuthBearerSaslClientAuthenticationProvider"); + setConf(conf); + } + + @Override public int run(String[] args) throws Exception { + LOG.info("JWT client example has been started"); + + Configuration conf = getConf(); + LOG.info("Config = " + conf.get("hbase.client.sasl.provider.class")); + UserProvider provider = UserProvider.instantiate(conf); + User user = provider.getCurrent(); + + OAuthBearerTokenUtil.addTokenForUser(user, JWT_TOKEN, 0); + LOG.info("JWT token added"); + + try (final Connection conn = ConnectionFactory.createConnection(conf, user)) { + LOG.info("Connected to HBase"); + Admin admin = conn.getAdmin(); + + TableName tn = TableName.valueOf("jwt-test-table"); + if (!admin.isTableAvailable(tn)) { + TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(tn) + .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(FAMILY).build()) + .build(); + admin.createTable(tableDescriptor); + } + + Table table = conn.getTable(tn); + byte[] rk = Bytes.toBytes(ThreadLocalRandom.current().nextLong()); + Put p = new Put(rk); + p.add(CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY) + .setRow(rk) + .setFamily(FAMILY) + .setType(Cell.Type.Put) + .setValue("test".getBytes(StandardCharsets.UTF_8)) + .build()); + table.put(p); + + admin.disableTable(tn); + admin.deleteTable(tn); + } + + LOG.info("JWT client example is done"); + return 0; + } + + public static void main(String[] args) throws Exception { + ToolRunner.run(new JwtClientExample(), args); + } +} diff --git a/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm b/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm index ac7c4a75e436..fa48fe049b90 100644 --- a/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm +++ b/hbase-resource-bundle/src/main/resources/META-INF/LICENSE.vm @@ -1343,7 +1343,7 @@ You can redistribute it and/or modify it under either the terms of the ## See this FAQ link for justifications: https://www.apache.org/legal/resolved.html ## ## NB: This list is later compared as lower-case. New entries must also be all lower-case -#set($non_aggregate_fine = [ 'public domain', 'new bsd license', 'bsd license', 'bsd', 'bsd 2-clause license', 'mozilla public license version 1.1', 'mozilla public license version 2.0', 'creative commons attribution license, version 2.5', 'apache-2.0' ]) +#set($non_aggregate_fine = [ 'public domain', 'new bsd license', 'bsd license', 'bsd', 'bsd 2-clause license', 'bsd-3-clause', 'mozilla public license version 1.1', 'mozilla public license version 2.0', 'creative commons attribution license, version 2.5', 'apache-2.0' ]) ## include LICENSE sections for anything not under ASL2.0 #foreach( ${dep} in ${projects} ) ## if there are no licenses we'll fail the build later, so diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml index 619ae146e425..c6d8870928a1 100644 --- a/hbase-server/pom.xml +++ b/hbase-server/pom.xml @@ -534,6 +534,10 @@ log4j-1.2-api test + + com.nimbusds + nimbus-jose-jwt + diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java index 4ebc9fa5325a..bddd3c55edee 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/ipc/ServerRpcConnection.java @@ -365,7 +365,7 @@ public void saslReadAndProcess(ByteBuff saslToken) throws IOException, throw e; } RpcServer.LOG.debug("Created SASL server with mechanism={}", - provider.getSaslAuthMethod().getAuthMethod()); + provider.getSaslAuthMethod().getSaslMechanism()); } RpcServer.LOG.debug("Read input token of size={} for processing by saslServer." + "evaluateResponse()", saslToken.limit()); diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java new file mode 100644 index 000000000000..ec4b3c2a329e --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import java.util.Objects; +import javax.security.auth.callback.Callback; +import org.apache.commons.lang3.StringUtils; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * A {@code Callback} for use by the {@code SaslServer} implementation when it + * needs to provide an OAuth 2 bearer token compact serialization for + * validation. Callback handlers should use the + * {@link #error(String, String, String)} method to communicate errors back to + * the SASL Client as per + * RFC 6749: The OAuth + * 2.0 Authorization Framework and the IANA + * OAuth Extensions Error Registry. Callback handlers should communicate + * other problems by raising an {@code IOException}. + *

+ * This class was introduced in 2.0.0 and, while it feels stable, it could + * evolve. We will try to evolve the API in a compatible manner, but we reserve + * the right to make breaking changes in minor releases, if necessary. We will + * update the {@code InterfaceStability} annotation and this notice once the API + * is considered stable. + */ +@InterfaceAudience.Public +public class OAuthBearerValidatorCallback implements Callback { + private final String tokenValue; + private OAuthBearerToken token = null; + private String errorStatus = null; + private String errorScope = null; + private String errorOpenIDConfiguration = null; + + /** + * Constructor + * + * @param tokenValue + * the mandatory/non-blank token value + */ + public OAuthBearerValidatorCallback(String tokenValue) { + if (StringUtils.isEmpty(tokenValue)) { + throw new IllegalArgumentException("token value must not be empty"); + } + this.tokenValue = tokenValue; + } + + /** + * Return the (always non-null) token value + * + * @return the (always non-null) token value + */ + public String tokenValue() { + return tokenValue; + } + + /** + * Return the (potentially null) token + * + * @return the (potentially null) token + */ + public OAuthBearerToken token() { + return token; + } + + /** + * Return the (potentially null) error status value as per + * RFC 7628: A Set + * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth + * and the IANA + * OAuth Extensions Error Registry. + * + * @return the (potentially null) error status value + */ + public String errorStatus() { + return errorStatus; + } + + /** + * Return the (potentially null) error scope value as per + * RFC 7628: A Set + * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth. + * + * @return the (potentially null) error scope value + */ + public String errorScope() { + return errorScope; + } + + /** + * Return the (potentially null) error openid-configuration value as per + * RFC 7628: A Set + * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth. + * + * @return the (potentially null) error openid-configuration value + */ + public String errorOpenIDConfiguration() { + return errorOpenIDConfiguration; + } + + /** + * Set the token. The token value is unchanged and is expected to match the + * provided token's value. All error values are cleared. + * + * @param token + * the mandatory token to set + */ + public void token(OAuthBearerToken token) { + this.token = Objects.requireNonNull(token); + this.errorStatus = null; + this.errorScope = null; + this.errorOpenIDConfiguration = null; + } + + /** + * Set the error values as per + * RFC 7628: A Set + * of Simple Authentication and Security Layer (SASL) Mechanisms for OAuth. + * Any token is cleared. + * + * @param errorStatus + * the mandatory error status value from the IANA + * OAuth Extensions Error Registry to set + * @param errorScope + * the optional error scope value to set + * @param errorOpenIDConfiguration + * the optional error openid-configuration value to set + */ + public void error(String errorStatus, String errorScope, String errorOpenIDConfiguration) { + if (StringUtils.isEmpty(errorStatus)) { + throw new IllegalArgumentException("error status must not be empty"); + } + this.errorStatus = errorStatus; + this.errorScope = errorScope; + this.errorOpenIDConfiguration = errorOpenIDConfiguration; + this.token = null; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java new file mode 100644 index 000000000000..3a56d5deb7c7 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.shaded.json.JSONObject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.SaslUtil; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code SaslServer} implementation for SASL/OAUTHBEARER in Kafka. An instance + * of {@link OAuthBearerToken} is available upon successful authentication via + * the negotiated property "{@code OAUTHBEARER.token}"; the token could be used + * in a custom authorizer (to authorize based on JWT claims rather than ACLs, + * for example). + */ +@InterfaceAudience.Public +public class OAuthBearerSaslServer implements SaslServer { + public static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslServer.class); + private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAUTHBEARER_MECHANISM + ".token"; + private static final String INTERNAL_ERROR_ON_SERVER = + "Authentication could not be performed due to an internal error on the server"; + static final String CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY = + "CREDENTIAL.LIFETIME.MS"; + + private final AuthenticateCallbackHandler callbackHandler; + + private boolean complete; + private OAuthBearerToken tokenForNegotiatedProperty = null; + private String errorMessage = null; + + public OAuthBearerSaslServer(CallbackHandler callbackHandler) { + if (!(callbackHandler instanceof AuthenticateCallbackHandler)) { + throw new IllegalArgumentException( + String.format("Callback handler must be castable to %s: %s", + AuthenticateCallbackHandler.class.getName(), callbackHandler.getClass().getName())); + } + this.callbackHandler = (AuthenticateCallbackHandler) callbackHandler; + } + + /** + * @throws SaslAuthenticationException + * if access token cannot be validated + *

+ * Note: This method may throw + * {@link SaslAuthenticationException} to provide custom error + * messages to clients. But care should be taken to avoid including + * any information in the exception message that should not be + * leaked to unauthenticated clients. It may be safer to throw + * {@link SaslException} in some cases so that a standard error + * message is returned to clients. + *

+ */ + @Override + public byte[] evaluateResponse(byte[] response) + throws SaslException, SaslAuthenticationException { + try { + if (response.length == 1 && response[0] == OAuthBearerSaslClient.BYTE_CONTROL_A && + errorMessage != null) { + LOG.error("Received %x01 response from client after it received our error"); + throw new SaslAuthenticationException(errorMessage); + } + errorMessage = null; + + OAuthBearerClientInitialResponse clientResponse; + clientResponse = new OAuthBearerClientInitialResponse(response); + + return process(clientResponse.tokenValue(), clientResponse.authorizationId()); + } catch (SaslAuthenticationException e) { + LOG.error("SASL authentication error", e); + throw e; + } catch (Exception e) { + LOG.error("SASL server problem", e); + throw e; + } + } + + @Override + public String getAuthorizationID() { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return tokenForNegotiatedProperty.principalName(); + } + + @Override + public String getMechanismName() { + return OAUTHBEARER_MECHANISM; + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + if (NEGOTIATED_PROPERTY_KEY_TOKEN.equals(propName)) { + return tokenForNegotiatedProperty; + } + if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { + return tokenForNegotiatedProperty.lifetimeMs(); + } + if (Sasl.QOP.equals(propName)) { + return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop(); + } + return null; + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(incoming, offset, offset + len); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(outgoing, offset, offset + len); + } + + @Override + public void dispose() { + complete = false; + tokenForNegotiatedProperty = null; + } + + private byte[] process(String tokenValue, String authorizationId) + throws SaslException { + OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(tokenValue); + try { + callbackHandler.handle(new Callback[] {callback}); + } catch (IOException | UnsupportedCallbackException e) { + handleCallbackError(e); + } + OAuthBearerToken token = callback.token(); + if (token == null) { + errorMessage = jsonErrorResponse(callback.errorStatus(), callback.errorScope(), + callback.errorOpenIDConfiguration()); + LOG.error("JWT token validation error: {}", errorMessage); + return errorMessage.getBytes(StandardCharsets.UTF_8); + } + /* + * We support the client specifying an authorization ID as per the SASL + * specification, but it must match the principal name if it is specified. + */ + if (!authorizationId.isEmpty() && !authorizationId.equals(token.principalName())) { + throw new SaslAuthenticationException(String.format( + "Authentication failed: Client requested an authorization id (%s) that is different from " + + "the token's principal name (%s)", + authorizationId, token.principalName())); + } + + tokenForNegotiatedProperty = token; + complete = true; + LOG.debug("Successfully authenticate User={}", token.principalName()); + return new byte[0]; + } + + private static String jsonErrorResponse(String errorStatus, String errorScope, + String errorOpenIDConfiguration) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("status", errorStatus); + if (!StringUtils.isBlank(errorScope)) { + jsonObject.put("scope", errorScope); + } + if (!StringUtils.isBlank(errorOpenIDConfiguration)) { + jsonObject.put("openid-configuration", errorOpenIDConfiguration); + } + return jsonObject.toJSONString(); + } + + private void handleCallbackError(Exception e) throws SaslException { + String msg = String.format("%s: %s", INTERNAL_ERROR_ON_SERVER, e.getMessage()); + LOG.debug(msg, e); + throw new SaslException(msg); + } + + public static class OAuthBearerSaslServerFactory implements SaslServerFactory { + @Override + public SaslServer createSaslServer(String mechanism, String protocol, String serverName, + Map props, CallbackHandler callbackHandler) { + String[] mechanismNamesCompatibleWithPolicy = getMechanismNames(props); + for (String s : mechanismNamesCompatibleWithPolicy) { + if (s.equals(mechanism)) { + return new OAuthBearerSaslServer(callbackHandler); + } + } + return null; + } + + @Override + public String[] getMechanismNames(Map props) { + return OAuthBearerUtils.mechanismNamesCompatibleWithPolicy(props); + } + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java new file mode 100644 index 000000000000..99f7df223b45 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import java.security.Provider; +import java.security.Security; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public class OAuthBearerSaslServerProvider extends Provider { + private static final long serialVersionUID = 1L; + + protected OAuthBearerSaslServerProvider() { + super("SASL/OAUTHBEARER Server Provider", 1.0, "SASL/OAUTHBEARER Server Provider for HBase"); + put("SaslServerFactory." + OAUTHBEARER_MECHANISM, + OAuthBearerSaslServer.OAuthBearerSaslServerFactory.class.getName()); + } + + public static void initialize() { + Security.addProvider(new OAuthBearerSaslServerProvider()); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java new file mode 100644 index 000000000000..acd5b047e1c8 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Exception thrown when there is a problem with the configuration (an invalid + * option in a JAAS config, for example). + */ +@InterfaceAudience.Public +public class OAuthBearerConfigException extends RuntimeException { + private static final long serialVersionUID = -8056105648062343518L; + + public OAuthBearerConfigException(String s) { + super(s); + } + + public OAuthBearerConfigException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java new file mode 100644 index 000000000000..09aa28c57ee4 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import java.util.Objects; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Exception thrown when token validation fails due to a problem with the token + * itself (as opposed to a missing remote resource or a configuration problem) + */ +@InterfaceAudience.Public +public class OAuthBearerIllegalTokenException extends RuntimeException { + private static final long serialVersionUID = -5275276640051316350L; + private final OAuthBearerValidationResult reason; + + /** + * Constructor + * + * @param reason + * the mandatory reason for the validation failure; it must indicate + * failure + */ + public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason) { + super(Objects.requireNonNull(reason, "Reason cannot be null").failureDescription()); + if (reason.success()) { + throw new IllegalArgumentException( + "The reason indicates success; it must instead indicate failure"); + } + this.reason = reason; + } + + public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason, Throwable t) { + super(Objects.requireNonNull(reason, "Reason cannot be null").failureDescription(), t); + if (reason.success()) { + throw new IllegalArgumentException( + "The reason indicates success; it must instead indicate failure"); + } + this.reason = reason; + } + + /** + * Return the (always non-null) reason for the validation failure + * + * @return the reason for the validation failure + */ + public OAuthBearerValidationResult reason() { + return reason; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java new file mode 100644 index 000000000000..f17457d66edc --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import java.text.ParseException; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Signed JWT implementation for OAuth Bearer authentication mech of SASL. + * + * This class is based on Kafka's Unsecured JWS token implementation. + */ +@InterfaceAudience.Public +public class OAuthBearerSignedJwt implements OAuthBearerToken { + private final String compactSerialization; + private final JWKSet jwkSet; + + private JWTClaimsSet claims; + private long lifetime; + private int maxClockSkewSeconds = 0; + private String requiredAudience; + private String requiredIssuer; + + /** + * Constructor base64 encoded JWT token and JWK Set. + * + * @param compactSerialization + * the compact serialization to parse as a signed JWT + * @param jwkSet + * the key set which the signature of this JWT should be verified with + */ + public OAuthBearerSignedJwt(String compactSerialization, JWKSet jwkSet) { + this.jwkSet = jwkSet; + this.compactSerialization = Objects.requireNonNull(compactSerialization); + } + + @Override + public String value() { + return compactSerialization; + } + + @Override + public String principalName() { + return claims.getSubject(); + } + + @Override + public long lifetimeMs() { + return lifetime; + } + + /** + * Return the JWT Claim Set as a {@code Map} + * + * @return the (always non-null but possibly empty) claims + */ + public Map claims() { + return claims.getClaims(); + } + + /** + * Set required audience, as per + * + * RFC7519 Section 4.1.3 + */ + public OAuthBearerSignedJwt audience(String aud) { + this.requiredAudience = aud; + return this; + } + + /** + * Set required issuer, as per + * + * RFC7519 Section 4.1.1 + */ + public OAuthBearerSignedJwt issuer(String iss) { + this.requiredIssuer = iss; + return this; + } + + /** + * Set maximum clock skew in seconds. + * @param value New value + */ + public OAuthBearerSignedJwt maxClockSkewSeconds(int value) { + this.maxClockSkewSeconds = value; + return this; + } + + /** + * This method provides a single method for validating the JWT for use in + * request processing. + * + * @throws OAuthBearerIllegalTokenException + * if the compact serialization is not a valid JWT + * (meaning it did not have 3 dot-separated Base64URL sections + * with a digital signature; or the header or claims + * either are not valid Base 64 URL encoded values or are not JSON + * after decoding; or the mandatory '{@code alg}' header value is + * missing) + */ + public OAuthBearerSignedJwt validate(){ + try { + this.claims = validateToken(compactSerialization); + Date expirationTimeSeconds = claims.getExpirationTime(); + if (expirationTimeSeconds == null) { + throw new OAuthBearerIllegalTokenException( + OAuthBearerValidationResult.newFailure("No expiration time in JWT")); + } + lifetime = expirationTimeSeconds.toInstant().toEpochMilli(); + String principalName = claims.getSubject(); + if (StringUtils.isBlank(principalName)) { + throw new OAuthBearerIllegalTokenException(OAuthBearerValidationResult + .newFailure("No principal name in JWT claim")); + } + return this; + } catch (ParseException | BadJOSEException | JOSEException e) { + throw new OAuthBearerIllegalTokenException( + OAuthBearerValidationResult.newFailure("Token validation failed: " + e.getMessage()), e); + } + } + + private JWTClaimsSet validateToken(String jwtToken) + throws BadJOSEException, JOSEException, ParseException { + JWT jwt = JWTParser.parse(jwtToken); + ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); + + Set requiredClaims = new HashSet<>(); + JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder(); + + // Audience + if (!StringUtils.isBlank(requiredAudience)) { + requiredClaims.add("aud"); + jwtClaimsSetBuilder.audience(requiredAudience); + } + + // Issuer + if (!StringUtils.isBlank(requiredIssuer)) { + requiredClaims.add("iss"); + jwtClaimsSetBuilder.issuer(requiredIssuer); + } + + // Subject / Principal is always required + requiredClaims.add("sub"); + + DefaultJWTClaimsVerifier jwtClaimsSetVerifier = + new DefaultJWTClaimsVerifier<>(jwtClaimsSetBuilder.build(), requiredClaims); + jwtClaimsSetVerifier.setMaxClockSkew(maxClockSkewSeconds); + jwtProcessor.setJWTClaimsSetVerifier(jwtClaimsSetVerifier); + + JWSKeySelector keySelector = + new JWSVerificationKeySelector<>((JWSAlgorithm)jwt.getHeader().getAlgorithm(), + new ImmutableJWKSet<>(jwkSet)); + jwtProcessor.setJWSKeySelector(keySelector); + return jwtProcessor.process(jwtToken, null); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java new file mode 100644 index 000000000000..8fb33ef5a2f1 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.jwk.JWKSet; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.text.ParseException; +import java.util.Map; +import java.util.Objects; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@code CallbackHandler} that recognizes + * {@link OAuthBearerValidatorCallback} and validates a secure (signed) OAuth 2 + * bearer token (JWT). + * + * It requires a valid JWK Set to be initialized at startup which holds the available + * RSA public keys that JWT signature can be validated with. The Set can be initialized + * via an URL or a local file. + * + * It requires there to be an "exp" (Expiration Time) + * claim of type Number. If "iat" (Issued At) or + * "nbf" (Not Before) claims are present each must be a number that + * precedes the Expiration Time claim, and if both are present the Not Before + * claim must not precede the Issued At claim. It also accepts the following + * options, none of which are required: + *
    + *
  • {@code hbase.security.oauth.jwt.jwks.url} set to a non-empty value if you + * wish to initialize the JWK Set via an URL. HTTPS URLs must have valid certificates. + *
  • + *
  • {@code hbase.security.oauth.jwt.jwks.file} set to a non-empty value if you + * wish to initialize the JWK Set from a local JSON file. + *
  • + *
  • {@code hbase.security.oauth.jwt.audience} set to a String value which + * you want the desired audience ("aud") the JWT to have.
  • + *
  • {@code hbase.security.oauth.jwt.issuer} set to a String value which + * you want the issuer ("iss") of the JWT has to be.
  • + *
  • {@code hbase.security.oauth.jwt.allowableclockskewseconds} set to a positive integer + * value if you wish to allow up to some number of positive seconds of + * clock skew (the default is 0)
  • + *
+ * + * This class is based on Kafka's OAuthBearerUnsecuredValidatorCallbackHandler. + */ +@InterfaceAudience.Public +public class OAuthBearerSignedJwtValidatorCallbackHandler implements AuthenticateCallbackHandler { + private static final Logger LOG = + LoggerFactory.getLogger(OAuthBearerSignedJwtValidatorCallbackHandler.class); + private static final String OPTION_PREFIX = "hbase.security.oauth.jwt."; + private static final String JWKS_URL = OPTION_PREFIX + "jwks.url"; + private static final String JWKS_FILE = OPTION_PREFIX + "jwks.file"; + private static final String ALLOWABLE_CLOCK_SKEW_SECONDS_OPTION = + OPTION_PREFIX + "allowableclockskewseconds"; + static final String REQUIRED_AUDIENCE_OPTION = OPTION_PREFIX + "audience"; + static final String REQUIRED_ISSUER_OPTION = OPTION_PREFIX + "issuer"; + private Configuration hBaseConfiguration; + private JWKSet jwkSet; + private boolean configured = false; + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + if (!configured) { + throw new RuntimeException( + "OAuthBearerSignedJwtValidatorCallbackHandler must be configured first."); + } + + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback) { + OAuthBearerValidatorCallback validationCallback = (OAuthBearerValidatorCallback) callback; + try { + handleCallback(validationCallback); + } catch (OAuthBearerIllegalTokenException e) { + LOG.error("Signed JWT token validation error: {}", e.getMessage()); + OAuthBearerValidationResult failureReason = e.reason(); + String failureScope = failureReason.failureScope(); + validationCallback.error(failureScope != null ? "insufficient_scope" : "invalid_token", + failureScope, failureReason.failureOpenIdConfig()); + } + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + @Override public void configure(Configuration configs, String saslMechanism, + Map saslProps) { + if (!OAUTHBEARER_MECHANISM.equals(saslMechanism)) { + throw new IllegalArgumentException( + String.format("Unexpected SASL mechanism: %s", saslMechanism)); + } + + this.hBaseConfiguration = configs; + + try { + loadJwkSet(); + } catch (IOException | ParseException e) { + throw new RuntimeException("Unable to initialize JWK Set", e); + } + + configured = true; + } + + @InterfaceAudience.Private + public void configure(Configuration configs, JWKSet jwkSet) { + this.hBaseConfiguration = Objects.requireNonNull(configs); + this.jwkSet = Objects.requireNonNull(jwkSet); + this.configured = true; + } + + private void handleCallback(OAuthBearerValidatorCallback callback) { + String tokenValue = callback.tokenValue(); + if (tokenValue == null) { + throw new IllegalArgumentException("Callback missing required token value"); + } + OAuthBearerSignedJwt signedJwt = new OAuthBearerSignedJwt(tokenValue, jwkSet) + .audience(requiredAudience()) + .issuer(requiredIssuer()) + .maxClockSkewSeconds(allowableClockSkewSeconds()) + .validate(); + + LOG.info("Successfully validated token with principal {}: {}", signedJwt.principalName(), + signedJwt.claims()); + callback.token(signedJwt); + } + + private String requiredAudience() { + return hBaseConfiguration.get(REQUIRED_AUDIENCE_OPTION); + } + + private String requiredIssuer() { + return hBaseConfiguration.get(REQUIRED_ISSUER_OPTION); + } + + private int allowableClockSkewSeconds() { + String allowableClockSkewSecondsValue = hBaseConfiguration.get( + ALLOWABLE_CLOCK_SKEW_SECONDS_OPTION); + int allowableClockSkewSeconds = 0; + try { + allowableClockSkewSeconds = StringUtils.isBlank(allowableClockSkewSecondsValue) + ? 0 : Integer.parseInt(allowableClockSkewSecondsValue.trim()); + } catch (NumberFormatException e) { + throw new OAuthBearerConfigException(e.getMessage(), e); + } + if (allowableClockSkewSeconds < 0) { + throw new OAuthBearerConfigException( + String.format("Allowable clock skew seconds must not be negative: %s", + allowableClockSkewSecondsValue)); + } + return allowableClockSkewSeconds; + } + + private void loadJwkSet() throws IOException, ParseException { + String jwksFile = hBaseConfiguration.get(JWKS_FILE); + String jwksUrl = hBaseConfiguration.get(JWKS_URL); + + if (StringUtils.isBlank(jwksFile) && StringUtils.isBlank(jwksUrl)) { + throw new RuntimeException("Failed to initialize JWKS db. " + + JWKS_FILE + " or " + JWKS_URL + " must be specified in the config."); + } + + if (!StringUtils.isBlank(jwksFile)) { + this.jwkSet = JWKSet.load(new File(jwksFile)); + LOG.debug("JWKS db initialized from file: {}", jwksFile); + return; + } + + this.jwkSet = JWKSet.load(new URL(jwksUrl)); + LOG.debug("JWKS db initialized from URL: {}", jwksUrl); + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java new file mode 100644 index 000000000000..d41962d5e67b --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import java.io.Serializable; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * The result of some kind of token validation + * + * This class has been copy-and-pasted from Kafka codebase. + */ +@InterfaceAudience.Public +public final class OAuthBearerValidationResult implements Serializable { + private static final long serialVersionUID = 5774669940899777373L; + private final boolean success; + private final String failureDescription; + private final String failureScope; + private final String failureOpenIdConfig; + + /** + * Return an instance indicating success + * + * @return an instance indicating success + */ + public static OAuthBearerValidationResult newSuccess() { + return new OAuthBearerValidationResult(true, null, null, null); + } + + /** + * Return a new validation failure instance + * + * @param failureDescription + * optional description of the failure + * @return a new validation failure instance + */ + public static OAuthBearerValidationResult newFailure(String failureDescription) { + return newFailure(failureDescription, null, null); + } + + /** + * Return a new validation failure instance + * + * @param failureDescription + * optional description of the failure + * @param failureScope + * optional scope to be reported with the failure + * @param failureOpenIdConfig + * optional OpenID Connect configuration to be reported with the + * failure + * @return a new validation failure instance + */ + public static OAuthBearerValidationResult newFailure(String failureDescription, + String failureScope, String failureOpenIdConfig) { + return new OAuthBearerValidationResult(false, failureDescription, failureScope, + failureOpenIdConfig); + } + + private OAuthBearerValidationResult(boolean success, String failureDescription, + String failureScope, String failureOpenIdConfig) { + if (success && (failureScope != null || failureOpenIdConfig != null)) { + throw new IllegalArgumentException( + "success was indicated but failure scope/OpenIdConfig were provided"); + } + this.success = success; + this.failureDescription = failureDescription; + this.failureScope = failureScope; + this.failureOpenIdConfig = failureOpenIdConfig; + } + + /** + * Return true if this instance indicates success, otherwise false + * + * @return true if this instance indicates success, otherwise false + */ + public boolean success() { + return success; + } + + /** + * Return the (potentially null) descriptive message for the failure + * + * @return the (potentially null) descriptive message for the failure + */ + public String failureDescription() { + return failureDescription; + } + + /** + * Return the (potentially null) scope to be reported with the failure + * + * @return the (potentially null) scope to be reported with the failure + */ + public String failureScope() { + return failureScope; + } + + /** + * Return the (potentially null) OpenID Connect configuration to be reported + * with the failure + * + * @return the (potentially null) OpenID Connect configuration to be reported + * with the failure + */ + public String failureOpenIdConfig() { + return failureOpenIdConfig; + } + + /** + * Raise an exception if this instance indicates failure, otherwise do nothing + * + * @throws OAuthBearerIllegalTokenException + * if this instance indicates failure + */ + public void throwExceptionIfFailed() throws OAuthBearerIllegalTokenException { + if (!success()) { + throw new OAuthBearerIllegalTokenException(this); + } + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java new file mode 100644 index 000000000000..b6f8078ccbe8 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.internals.OAuthBearerSaslServerProvider; +import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.token.SecretManager; +import org.apache.hadoop.security.token.TokenIdentifier; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@InterfaceAudience.Private +public class OAuthBearerSaslServerAuthenticationProvider + extends OAuthBearerSaslAuthenticationProvider + implements SaslServerAuthenticationProvider { + + private static final Logger LOG = LoggerFactory.getLogger( + OAuthBearerSaslServerAuthenticationProvider.class); + private Configuration hbaseConfiguration; + private boolean initialized = false; + + static { + OAuthBearerSaslServerProvider.initialize(); // not part of public API + LOG.info("OAuthBearer SASL server provider has been initialized"); + } + + @Override public void init(Configuration conf) throws IOException { + this.hbaseConfiguration = conf; + this.initialized = true; + } + + @Override public AttemptingUserProvidingSaslServer createServer( + SecretManager secretManager, Map saslProps) + throws IOException { + + if (!initialized) { + throw new IllegalStateException( + "OAuthBearerSaslServerAuthenticationProvider must be initialized first."); + } + + UserGroupInformation current = UserGroupInformation.getCurrentUser(); + String fullName = current.getUserName(); + LOG.debug("Server's OAuthBearer user name is {}", fullName); + LOG.debug("OAuthBearer saslProps = {}", saslProps); + + try { + return current.doAs(new PrivilegedExceptionAction() { + @Override + public AttemptingUserProvidingSaslServer run() throws SaslException { + AuthenticateCallbackHandler callbackHandler = + new OAuthBearerSignedJwtValidatorCallbackHandler(); + callbackHandler.configure(hbaseConfiguration, getSaslAuthMethod().getSaslMechanism(), + saslProps); + return new AttemptingUserProvidingSaslServer(Sasl.createSaslServer( + getSaslAuthMethod().getSaslMechanism(), null, null, saslProps, + callbackHandler), () -> null); + } + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException("Failed to construct OAUTHBEARER SASL server"); + } + } + + @Override public boolean supportsProtocolAuthentication() { + return true; + } + + @Override public UserGroupInformation getAuthorizedUgi(String authzId, + SecretManager secretManager) { + UserGroupInformation ugi = UserGroupInformation.createRemoteUser(authzId); + ugi.setAuthenticationMethod(getSaslAuthMethod().getAuthMethod()); + return ugi; + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java new file mode 100644 index 000000000000..9e6670b19d30 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +public class OAuthBearerTokenMock implements OAuthBearerToken { + @Override + public String value() { + return null; + } + + @Override + public long lifetimeMs() { + return 0; + } + + @Override + public String principalName() { + return null; + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java new file mode 100644 index 000000000000..8be3cfcaf4a4 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerValidatorCallbackTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerValidatorCallbackTest.class); + + private static final OAuthBearerToken TOKEN = new OAuthBearerToken() { + @Override + public String value() { + return "value"; + } + + @Override + public String principalName() { + return "principalName"; + } + + @Override + public long lifetimeMs() { + return 0; + } + }; + + @Test + public void testError() { + String errorStatus = "errorStatus"; + String errorScope = "errorScope"; + String errorOpenIDConfiguration = "errorOpenIDConfiguration"; + OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(TOKEN.value()); + callback.error(errorStatus, errorScope, errorOpenIDConfiguration); + assertEquals(errorStatus, callback.errorStatus()); + assertEquals(errorScope, callback.errorScope()); + assertEquals(errorOpenIDConfiguration, callback.errorOpenIDConfiguration()); + assertNull(callback.token()); + } + + @Test + public void testToken() { + OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(TOKEN.value()); + callback.token(TOKEN); + assertSame(TOKEN, callback.token()); + assertNull(callback.errorStatus()); + assertNull(callback.errorScope()); + assertNull(callback.errorOpenIDConfiguration()); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java new file mode 100644 index 000000000000..377371e7dbcd --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils.USER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import java.nio.charset.StandardCharsets; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerConfigException; +import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerSaslServerTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerSaslServerTest.class); + + private static final Configuration CONFIGS; + static { + CONFIGS = new Configuration(); + } + + private String JWT; + private OAuthBearerSaslServer saslServer; + + @Before + public void setUp() throws JOSEException { + RSAKey rsaKey = JwtTestUtils.generateRSAKey(); + JWT = JwtTestUtils.createSignedJwt(rsaKey); + OAuthBearerSignedJwtValidatorCallbackHandler validatorCallbackHandler = + new OAuthBearerSignedJwtValidatorCallbackHandler(); + validatorCallbackHandler.configure(CONFIGS, new JWKSet(rsaKey)); + // only validate extensions "firstKey" and "secondKey" + saslServer = new OAuthBearerSaslServer(validatorCallbackHandler); + } + + @Test + public void noAuthorizationIdSpecified() throws Exception { + byte[] nextChallenge = saslServer + .evaluateResponse(clientInitialResponse(null)); + // also asserts that no authentication error is thrown + // if OAuthBearerExtensionsValidatorCallback is not supported + assertTrue("Next challenge is not empty",nextChallenge.length == 0); + } + + @Test + public void negotiatedProperty() throws Exception { + saslServer.evaluateResponse(clientInitialResponse(USER)); + OAuthBearerToken token = + (OAuthBearerToken) saslServer.getNegotiatedProperty("OAUTHBEARER.token"); + assertNotNull(token); + assertEquals(token.lifetimeMs(), + saslServer.getNegotiatedProperty( + OAuthBearerSaslServer.CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY)); + } + + @Test + public void authorizatonIdEqualsAuthenticationId() throws Exception { + byte[] nextChallenge = saslServer + .evaluateResponse(clientInitialResponse(USER)); + assertTrue("Next challenge is not empty", nextChallenge.length == 0); + } + + @Test + public void authorizatonIdNotEqualsAuthenticationId() { + assertThrows(SaslAuthenticationException.class, + () -> saslServer.evaluateResponse(clientInitialResponse(USER + "x"))); + } + + @Test + public void illegalToken() throws Exception { + byte[] bytes = saslServer.evaluateResponse(clientInitialResponse(null, true)); + String challenge = new String(bytes, StandardCharsets.UTF_8); + assertEquals("{\"status\":\"invalid_token\"}", challenge); + } + + private byte[] clientInitialResponse(String authorizationId) + throws OAuthBearerConfigException { + return clientInitialResponse(authorizationId, false); + } + + private byte[] clientInitialResponse(String authorizationId, boolean illegalToken) + throws OAuthBearerConfigException { + String compactSerialization = JWT; + String tokenValue = compactSerialization + (illegalToken ? "AB" : ""); + return new OAuthBearerClientInitialResponse(tokenValue, authorizationId).toBytes(); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java new file mode 100644 index 000000000000..4b72f88bd6c4 --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerSignedJwtTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerSignedJwtTest.class); + private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles"); + private final static int EXP_DAYS = 10; + + private JWKSet JWK_SET; + private RSAKey RSA_KEY; + + @Before + public void before() throws JOSEException { + RSA_KEY = JwtTestUtils.generateRSAKey(); + JWK_SET = new JWKSet(RSA_KEY); + } + + @Test + public void validCompactSerialization() throws JOSEException { + String subject = "foo"; + + LocalDate issuedAt = LocalDate.now(ZONE_ID); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); + String validCompactSerialization = + compactSerialization(subject, issuedAt, expirationTime); + OAuthBearerSignedJwt jws = new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET) + .validate(); + assertEquals(5, jws.claims().size()); + assertEquals(subject, jws.claims().get("sub")); + assertEquals(issuedAt, Date.class.cast(jws.claims().get("iat")).toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate()); + assertEquals(expirationTime, Date.class.cast(jws.claims().get("exp")).toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate()); + assertEquals(expirationTime.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(), + jws.lifetimeMs()); + } + + @Test + public void missingPrincipal() throws JOSEException { + String subject = null; + LocalDate issuedAt = LocalDate.now(ZONE_ID); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); + String validCompactSerialization = + compactSerialization(subject, issuedAt, expirationTime); + assertThrows(OAuthBearerIllegalTokenException.class, + () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET).validate()); + } + + @Test + public void blankPrincipalName() throws JOSEException { + String subject = " "; + LocalDate issuedAt = LocalDate.now(ZONE_ID); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); + String validCompactSerialization = + compactSerialization(subject, issuedAt, expirationTime); + assertThrows(OAuthBearerIllegalTokenException.class, + () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET).validate()); + } + + @Test + public void missingIssuer() throws JOSEException { + String validCompactSerialization = + JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, ""); + assertThrows(OAuthBearerIllegalTokenException.class, + () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET) + .issuer("test-issuer") + .validate()); + } + + @Test + public void badIssuer() throws JOSEException { + String validCompactSerialization = + JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "bad-issuer"); + assertThrows(OAuthBearerIllegalTokenException.class, + () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET) + .issuer("test-issuer") + .validate()); + } + + private String compactSerialization(String subject, LocalDate issuedAt, LocalDate expirationTime) + throws JOSEException { + return JwtTestUtils.createSignedJwt(RSA_KEY, "me", subject, + expirationTime, issuedAt, "test-audience"); + } +} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java new file mode 100644 index 000000000000..aa6c8e63156e --- /dev/null +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import static org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler.REQUIRED_AUDIENCE_OPTION; +import static org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler.REQUIRED_ISSUER_OPTION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import java.time.LocalDate; +import java.time.ZoneId; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MiscTests.class, SmallTests.class}) +public class OAuthBearerSignedJwtValidatorCallbackHandlerTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerSignedJwtValidatorCallbackHandlerTest.class); + + private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles"); + private static final HBaseConfiguration EMPTY_CONFIG = new HBaseConfiguration(); + private static final HBaseConfiguration REQUIRED_AUDIENCE_CONFIG; + static { + REQUIRED_AUDIENCE_CONFIG = new HBaseConfiguration(); + REQUIRED_AUDIENCE_CONFIG.set(REQUIRED_AUDIENCE_OPTION, "test-audience"); + } + private static final HBaseConfiguration REQUIRED_ISSUER_CONFIG; + static { + REQUIRED_ISSUER_CONFIG = new HBaseConfiguration(); + REQUIRED_ISSUER_CONFIG.set(REQUIRED_ISSUER_OPTION, "test-issuer"); + } + + private RSAKey RSA_KEY; + + @Before + public void before() throws JOSEException { + RSA_KEY = JwtTestUtils.generateRSAKey(); + } + + @Test + public void validToken() throws JOSEException, UnsupportedCallbackException { + Object validationResult = validationResult(EMPTY_CONFIG, JwtTestUtils.createSignedJwt(RSA_KEY)); + assertTrue(validationResult instanceof OAuthBearerValidatorCallback); + assertTrue(((OAuthBearerValidatorCallback) validationResult).token() + instanceof OAuthBearerSignedJwt); + } + + @Test + public void missingPrincipal() + throws UnsupportedCallbackException, JOSEException { + LocalDate now = LocalDate.now(ZONE_ID); + String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", + now.plusDays(1), now, "test-aud"); + confirmFailsValidation(EMPTY_CONFIG, token); + } + + @Test + public void tooEarlyExpirationTime() throws JOSEException, UnsupportedCallbackException { + LocalDate now = LocalDate.now(ZONE_ID); + String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", + now.minusDays(1), + now.minusDays(1), + "test-aud"); + confirmFailsValidation(EMPTY_CONFIG, token); + } + + @Test + public void requiredAudience() throws JOSEException, UnsupportedCallbackException { + String token = JwtTestUtils.createSignedJwtWithAudience(RSA_KEY, "test-audience"); + Object validationResult = validationResult(REQUIRED_AUDIENCE_CONFIG, token); + assertTrue(validationResult instanceof OAuthBearerValidatorCallback); + assertTrue(((OAuthBearerValidatorCallback) validationResult).token() + instanceof OAuthBearerSignedJwt); + } + + @Test + public void missingAudience() throws JOSEException, UnsupportedCallbackException { + String token = JwtTestUtils.createSignedJwt(RSA_KEY); + confirmFailsValidation(REQUIRED_AUDIENCE_CONFIG, token); + } + + @Test + public void badAudience() throws JOSEException, UnsupportedCallbackException { + String token = JwtTestUtils.createSignedJwtWithAudience(RSA_KEY, "bad-audience"); + confirmFailsValidation(REQUIRED_AUDIENCE_CONFIG, token); + } + + @Test + public void requiredIssuer() throws UnsupportedCallbackException, JOSEException { + String token = JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "test-issuer"); + Object validationResult = validationResult(REQUIRED_ISSUER_CONFIG, token); + assertTrue(validationResult instanceof OAuthBearerValidatorCallback); + assertTrue(((OAuthBearerValidatorCallback) validationResult).token() + instanceof OAuthBearerSignedJwt); + } + + @Test + public void missingIssuer() throws JOSEException, UnsupportedCallbackException { + String token = JwtTestUtils.createSignedJwt(RSA_KEY); + confirmFailsValidation(REQUIRED_ISSUER_CONFIG, token); + } + + @Test + public void badIssuer() throws JOSEException, UnsupportedCallbackException { + String token = JwtTestUtils.createSignedJwtWithIssuer(RSA_KEY, "bad-issuer"); + confirmFailsValidation(REQUIRED_ISSUER_CONFIG, token); + } + + private void confirmFailsValidation(HBaseConfiguration config, String tokenValue) + throws OAuthBearerConfigException, OAuthBearerIllegalTokenException, + UnsupportedCallbackException { + Object validationResultObj = validationResult(config, tokenValue); + assertTrue(validationResultObj instanceof OAuthBearerValidatorCallback); + OAuthBearerValidatorCallback callback = (OAuthBearerValidatorCallback) validationResultObj; + assertNull(callback.token()); + assertNull(callback.errorOpenIDConfiguration()); + assertEquals("invalid_token", callback.errorStatus()); + assertNull(callback.errorScope()); + } + + private OAuthBearerValidatorCallback validationResult(HBaseConfiguration config, + String tokenValue) + throws UnsupportedCallbackException { + OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(tokenValue); + createCallbackHandler(config).handle(new Callback[] {callback}); + return callback; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private OAuthBearerSignedJwtValidatorCallbackHandler + createCallbackHandler(HBaseConfiguration config) { + OAuthBearerSignedJwtValidatorCallbackHandler callbackHandler = + new OAuthBearerSignedJwtValidatorCallbackHandler(); + callbackHandler.configure(config, new JWKSet(RSA_KEY)); + return callbackHandler; + } +} diff --git a/pom.xml b/pom.xml index 170b95c2b95d..7983cfa40e0f 100755 --- a/pom.xml +++ b/pom.xml @@ -1841,6 +1841,7 @@ 1.9 1.5.0-4 4.0.1 + 9.15.2 @@ -3441,12 +3442,16 @@ hadoop-distcp ${hadoop-three.version} - org.apache.hadoop hadoop-hdfs-client ${hadoop-three.version} + + com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} +