From d6a8c4db15365949398c4d0905be13dc803825a8 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Wed, 29 Sep 2021 18:02:29 +0200 Subject: [PATCH 01/15] HBASE-26553. Implement new OAuth Bearer SASL plugin --- ...OAuthBearerSaslAuthenticationProvider.java | 42 +++ ...earerSaslClientAuthenticationProvider.java | 160 +++++++++++ .../OAuthBearerSaslProviderSelector.java | 69 +++++ ...thBearerSaslClientCallbackHandlerTest.java | 98 +++++++ hbase-common/pom.xml | 4 + .../exceptions/IllegalSaslStateException.java | 40 +++ .../SaslAuthenticationException.java | 51 ++++ .../auth/AuthenticateCallbackHandler.java | 53 ++++ .../hbase/security/auth/SaslExtensions.java | 60 ++++ .../security/auth/SaslExtensionsCallback.java | 52 ++++ ...AuthBearerExtensionsValidatorCallback.java | 123 ++++++++ .../oauthbearer/OAuthBearerToken.java | 75 +++++ .../oauthbearer/OAuthBearerTokenCallback.java | 122 ++++++++ .../OAuthBearerValidatorCallback.java | 155 ++++++++++ .../hbase/security/oauthbearer/Utils.java | 91 ++++++ .../OAuthBearerClientInitialResponse.java | 212 ++++++++++++++ .../internals/OAuthBearerSaslClient.java | 210 ++++++++++++++ .../OAuthBearerSaslClientProvider.java | 38 +++ .../internals/OAuthBearerSaslServer.java | 270 ++++++++++++++++++ .../OAuthBearerSaslServerProvider.java | 38 +++ .../knox/OAuthBearerConfigException.java | 37 +++ .../OAuthBearerIllegalTokenException.java | 65 +++++ .../internals/knox/OAuthBearerSignedJwt.java | 199 +++++++++++++ ...arerSignedJwtValidatorCallbackHandler.java | 205 +++++++++++++ .../knox/OAuthBearerValidationResult.java | 135 +++++++++ .../security/token/OAuthBearerTokenUtil.java | 77 +++++ .../security/oauthbearer/JwtTestUtils.java | 116 ++++++++ ...BearerExtensionsValidatorCallbackTest.java | 94 ++++++ .../OAuthBearerTokenCallbackTest.java | 65 +++++ .../oauthbearer/OAuthBearerTokenMock.java | 34 +++ .../OAuthBearerValidatorCallbackTest.java | 65 +++++ .../OAuthBearerClientInitialResponseTest.java | 135 +++++++++ .../internals/OAuthBearerSaslClientTest.java | 126 ++++++++ .../internals/OAuthBearerSaslServerTest.java | 223 +++++++++++++++ .../knox/OAuthBearerSignedJwtTest.java | 109 +++++++ ...SignedJwtValidatorCallbackHandlerTest.java | 154 ++++++++++ .../jwt/client/example/JwtClientExample.java | 106 +++++++ .../src/main/resources/META-INF/LICENSE.vm | 2 +- .../hadoop/hbase/ipc/ServerRpcConnection.java | 2 +- ...earerSaslServerAuthenticationProvider.java | 99 +++++++ pom.xml | 11 +- 41 files changed, 4019 insertions(+), 3 deletions(-) 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/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/auth/SaslExtensions.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.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/OAuthBearerTokenCallback.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java create mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.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/OAuthBearerExtensionsValidatorCallbackTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java create mode 100644 hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.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/provider/OAuthBearerSaslServerAuthenticationProvider.java 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..466bcec0cd35 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientAuthenticationProvider.java @@ -0,0 +1,160 @@ +/* + * 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.OAUTHBEARER_MECHANISM; +import java.io.IOException; +import java.net.InetAddress; +import java.security.AccessController; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +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.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; +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 RuntimeException( + "OAuthBearerSaslClientCallbackHandler handler must be configured first."); + } + + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerTokenCallback) { + handleCallback((OAuthBearerTokenCallback) callback); + } else if (callback instanceof SaslExtensionsCallback) { + handleCallback((SaslExtensionsCallback) callback, + Subject.getSubject(AccessController.getContext())); + } 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(); + 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"); + callback.token(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. + */ + SortedSet sortedByLifetime = + new TreeSet<>( + new Comparator() { + @Override + public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { + return Long.compare(o1.lifetimeMs(), o2.lifetimeMs()); + } + }); + sortedByLifetime.addAll(privateCredentials); + LOG.warn("Found {} OAuth Bearer tokens in Subject's private credentials; " + + "the oldest expires at {}, will use the newest, which expires at {}", + sortedByLifetime.size(), + new Date(sortedByLifetime.first().lifetimeMs()), + new Date(sortedByLifetime.last().lifetimeMs())); + callback.token(sortedByLifetime.last()); + } + } + + /** + * Attaches the first {@link SaslExtensions} found in the public credentials of the Subject + */ + private static void handleCallback(SaslExtensionsCallback extensionsCallback, Subject subject) { + if (subject != null && !subject.getPublicCredentials(SaslExtensions.class).isEmpty()) { + SaslExtensions extensions = + subject.getPublicCredentials(SaslExtensions.class).iterator().next(); + extensionsCallback.extensions(extensions); + } + } + } + + @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/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..a780052767db --- /dev/null +++ b/hbase-client/src/test/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslClientCallbackHandlerTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.provider; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.Set; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil; +import org.junit.Test; + +public class OAuthBearerSaslClientCallbackHandlerTest { + private static OAuthBearerToken createTokenWithLifetimeMillis(final long lifetimeMillis) { + return new OAuthBearerToken() { + @Override + public String value() { + return null; + } + + @Override + public String principalName() { + return null; + } + + @Override + public long lifetimeMs() { + return lifetimeMillis; + } + }; + } + + @Test + public void testWithZeroTokens() { + OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = + createCallbackHandler(); + PrivilegedActionException e = + assertThrows(PrivilegedActionException.class, () -> Subject.doAs(new Subject(), + (PrivilegedExceptionAction) () -> { + 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)); + 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(), OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM, + Collections.emptyMap()); + return handler; + } +} diff --git a/hbase-common/pom.xml b/hbase-common/pom.xml index 95df377a78ab..47541ba9d0f4 100644 --- a/hbase-common/pom.xml +++ b/hbase-common/pom.xml @@ -255,6 +255,10 @@ kerb-simplekdc test + + com.nimbusds + nimbus-jose-jwt + 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..a18904603f5d --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/IllegalSaslStateException.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.exceptions; + +import org.apache.yetus.audience.InterfaceAudience; + +/** + * This exception indicates unexpected requests prior to SASL authentication. + * This could be due to misconfigured security, e.g. if PLAINTEXT protocol + * is used to connect to a SASL endpoint. + */ +@InterfaceAudience.Public +public class IllegalSaslStateException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + public IllegalSaslStateException(String message) { + super(message); + } + + public IllegalSaslStateException(String message, Throwable cause) { + super(message, cause); + } + +} 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..f7537d99581b --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/exceptions/SaslAuthenticationException.java @@ -0,0 +1,51 @@ +/* + * 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); + } + + public SaslAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + +} 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..6b966bbae19f --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/AuthenticateCallbackHandler.java @@ -0,0 +1,53 @@ +/* + * 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.Public +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) {} + + /** + * Closes this instance. + */ + default void close() {} +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java new file mode 100644 index 000000000000..c7683a9a0fc7 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java @@ -0,0 +1,60 @@ +/* + * 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 org.apache.yetus.audience.InterfaceAudience; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@InterfaceAudience.Public +public class SaslExtensions { + /** + * An "empty" instance indicating no SASL extensions + */ + public static final SaslExtensions NO_SASL_EXTENSIONS = new SaslExtensions(Collections.emptyMap()); + private final Map extensionsMap; + + public SaslExtensions(Map extensionsMap) { + this.extensionsMap = Collections.unmodifiableMap(new HashMap<>(extensionsMap)); + } + + /** + * Returns an immutable map of the extension names and their values + */ + public Map map() { + return extensionsMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return extensionsMap.equals(((SaslExtensions) o).extensionsMap); + } + + @Override + public String toString() { + return extensionsMap.toString(); + } + + @Override + public int hashCode() { + return extensionsMap.hashCode(); + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java new file mode 100644 index 000000000000..15f853abd175 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.auth; + +import java.util.Objects; +import javax.security.auth.callback.Callback; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Optional callback used for SASL mechanisms if any extensions need to be set + * in the SASL exchange. + */ +@InterfaceAudience.Public +public class SaslExtensionsCallback implements Callback { + private SaslExtensions extensions = SaslExtensions.NO_SASL_EXTENSIONS; + + /** + * Returns always non-null {@link SaslExtensions} consisting of the extension + * names and values that are sent by the client to the server in the initial + * client SASL authentication message. The default value is + * {@link SaslExtensions#NO_SASL_EXTENSIONS} so that if this callback is + * unhandled the client will see a non-null value. + */ + public SaslExtensions extensions() { + return extensions; + } + + /** + * Sets the SASL extensions on this callback. + * + * @param extensions + * the mandatory extensions to set + */ + public void extensions(SaslExtensions extensions) { + this.extensions = Objects.requireNonNull(extensions, "extensions must not be null"); + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java new file mode 100644 index 000000000000..a7e4d7dbb475 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import static org.apache.hadoop.hbase.security.oauthbearer.Utils.subtractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.security.auth.callback.Callback; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * A {@code Callback} for use by the {@code SaslServer} implementation when it + * needs to validate the SASL extensions for the OAUTHBEARER mechanism + * Callback handlers should use the {@link #valid(String)} + * method to communicate valid extensions back to the SASL server. + * Callback handlers should use the + * {@link #error(String, String)} method to communicate validation errors back to + * the SASL Server. + * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown extensions must be ignored by the server. + * The callback handler implementation should simply ignore unknown extensions, + * not calling {@link #error(String, String)} nor {@link #valid(String)}. + * Callback handlers should communicate other problems by raising an {@code IOException}. + *

+ * The OAuth bearer token is provided in the callback for better context in extension validation. + * It is very important that token validation is done in its own {@link OAuthBearerValidatorCallback} + * irregardless of provided extensions, as they are inherently insecure. + */ +@InterfaceAudience.Public +public class OAuthBearerExtensionsValidatorCallback implements Callback { + private final OAuthBearerToken token; + private final SaslExtensions inputExtensions; + private final Map validatedExtensions = new HashMap<>(); + private final Map invalidExtensions = new HashMap<>(); + + public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, SaslExtensions extensions) { + this.token = Objects.requireNonNull(token); + this.inputExtensions = Objects.requireNonNull(extensions); + } + + /** + * @return {@link OAuthBearerToken} the OAuth bearer token of the client + */ + public OAuthBearerToken token() { + return token; + } + + /** + * @return {@link SaslExtensions} consisting of the unvalidated extension names and values that + * were sent by the client + */ + public SaslExtensions inputExtensions() { + return inputExtensions; + } + + /** + * @return an unmodifiable {@link Map} consisting of the validated and recognized by the server + * extension names and values. + */ + public Map validatedExtensions() { + return Collections.unmodifiableMap(validatedExtensions); + } + + /** + * @return An immutable {@link Map} consisting of the name->error messages of extensions + * which failed validation + */ + public Map invalidExtensions() { + return Collections.unmodifiableMap(invalidExtensions); + } + + /** + * @return An immutable {@link Map} consisting of the extensions that have neither been + * validated nor invalidated + */ + public Map ignoredExtensions() { + return Collections.unmodifiableMap( + subtractMap(subtractMap(inputExtensions.map(), invalidExtensions), validatedExtensions)); + } + + /** + * Validates a specific extension in the original {@code inputExtensions} map + * @param extensionName - the name of the extension which was validated + */ + public void valid(String extensionName) { + if (!inputExtensions.map().containsKey(extensionName)) { + throw new IllegalArgumentException( + String.format("Extension %s was not found in the original extensions", extensionName)); + } + validatedExtensions.put(extensionName, inputExtensions.map().get(extensionName)); + } + /** + * Set the error value for a specific extension key-value pair if validation has failed + * + * @param invalidExtensionName + * the mandatory extension name which caused the validation failure + * @param errorMessage + * error message describing why the validation failed + */ + public void error(String invalidExtensionName, String errorMessage) { + if (Objects.requireNonNull(invalidExtensionName).isEmpty()) { + throw new IllegalArgumentException("extension name must not be empty"); + } + this.invalidExtensions.put(invalidExtensionName, errorMessage); + } +} 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..f581aaa5ed0d --- /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 #scope()}, {@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/OAuthBearerTokenCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java new file mode 100644 index 000000000000..51a76b509fa7 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import java.util.Objects; +import javax.security.auth.callback.Callback; +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 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 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 (Objects.requireNonNull(errorCode).isEmpty()) { + 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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java new file mode 100644 index 000000000000..a0469d121052 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.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.oauthbearer; + +import java.util.Objects; +import javax.security.auth.callback.Callback; +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 (Objects.requireNonNull(tokenValue).isEmpty()) { + 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 (Objects.requireNonNull(errorStatus).isEmpty()) { + 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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java new file mode 100644 index 000000000000..097046963aec --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public final class Utils { + /** + * Converts a {@code Map} class into a string, concatenating keys and values + * Example: + * {@code mkString({ key: "hello", keyTwo: "hi" }, "|START|", "|END|", "=", ",") + * => "|START|key=hello,keyTwo=hi|END|"} + */ + public static String mkString(Map map, String begin, String end, + String keyValueSeparator, String elementSeparator) { + StringBuilder bld = new StringBuilder(); + bld.append(begin); + String prefix = ""; + for (Map.Entry entry : map.entrySet()) { + bld.append(prefix).append(entry.getKey()). + append(keyValueSeparator).append(entry.getValue()); + prefix = elementSeparator; + } + bld.append(end); + return bld.toString(); + } + + /** + * 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; + } + + /** + * Given two maps (A, B), returns all the key-value pairs in A whose keys are not contained in B + */ + public static Map subtractMap(Map minuend, + Map subtrahend) { + return minuend.entrySet().stream() + .filter(entry -> !subtrahend.containsKey(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * Checks if a string is null, empty or whitespace only. + * @param str a string to be checked + * @return true if the string is null, empty or whitespace only; otherwise, return false. + */ + public static boolean isBlank(String str) { + return str == null || str.trim().isEmpty(); + } + + private Utils() { + // 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..4bc4ebf01761 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponse.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.Utils; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * OAuthBearer SASL client's initial message to the server. + * + * This class has been copy-and-pasted from Kafka codebase. + */ +@InterfaceAudience.Public +public class OAuthBearerClientInitialResponse { + static final String SEPARATOR = "\u0001"; + + private static final String SASLNAME = "(?:[\\x01-\\x7F&&[^=,]]|=2C|=3D)+"; + private static final String KEY = "[A-Za-z]+"; + private static final String VALUE = "[\\x21-\\x7E \t\r\n]+"; + + private static final String KVPAIRS = String.format("(%s=%s%s)*", KEY, VALUE, SEPARATOR); + private static final Pattern AUTH_PATTERN = + Pattern.compile("(?[\\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; + private final SaslExtensions saslExtensions; + + public static final Pattern EXTENSION_KEY_PATTERN = Pattern.compile(KEY); + public static final Pattern EXTENSION_VALUE_PATTERN = Pattern.compile(VALUE); + + public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { + String responseMsg = new String(response, StandardCharsets.UTF_8); + Matcher matcher = CLIENT_INITIAL_RESPONSE_PATTERN.matcher(responseMsg); + if (!matcher.matches()) { + throw new SaslException("Invalid OAUTHBEARER client first message"); + } + String authzid = matcher.group("authzid"); + this.authorizationId = authzid == null ? "" : authzid; + String kvPairs = matcher.group("kvpairs"); + Map properties = Utils.parseMap(kvPairs, "=", SEPARATOR); + String auth = properties.get(AUTH_KEY); + if (auth == null) { + throw new SaslException("Invalid OAUTHBEARER client first message: 'auth' not specified"); + } + properties.remove(AUTH_KEY); + SaslExtensions extensions = new SaslExtensions(properties); + validateExtensions(extensions); + this.saslExtensions = extensions; + + Matcher authMatcher = AUTH_PATTERN.matcher(auth); + if (!authMatcher.matches()) { + throw new SaslException("Invalid OAUTHBEARER client first message: invalid 'auth' format"); + } + if (!"bearer".equalsIgnoreCase(authMatcher.group("scheme"))) { + String msg = String.format("Invalid scheme in OAUTHBEARER client first message: %s", + matcher.group("scheme")); + throw new SaslException(msg); + } + this.tokenValue = authMatcher.group("token"); + } + + /** + * Constructor + * + * @param tokenValue + * the mandatory token value + * @param extensions + * the optional extensions + * @throws SaslException + * if any extension name or value fails to conform to the required + * regular expression as defined by the specification, or if the + * reserved {@code auth} appears as a key + */ + public OAuthBearerClientInitialResponse(String tokenValue, SaslExtensions extensions) + throws SaslException { + this(tokenValue, "", extensions); + } + + /** + * Constructor + * + * @param tokenValue + * the mandatory token value + * @param authorizationId + * the optional authorization ID + * @param extensions + * the optional extensions + * @throws SaslException + * if any extension name or value fails to conform to the required + * regular expression as defined by the specification, or if the + * reserved {@code auth} appears as a key + */ + public OAuthBearerClientInitialResponse(String tokenValue, String authorizationId, + SaslExtensions extensions) throws SaslException { + this.tokenValue = Objects.requireNonNull(tokenValue, "token value must not be null"); + this.authorizationId = authorizationId == null ? "" : authorizationId; + validateExtensions(extensions); + this.saslExtensions = extensions != null ? extensions : SaslExtensions.NO_SASL_EXTENSIONS; + } + + /** + * Return the always non-null extensions + * + * @return the always non-null extensions + */ + public SaslExtensions extensions() { + return saslExtensions; + } + + public byte[] toBytes() { + String authzid = authorizationId.isEmpty() ? "" : "a=" + authorizationId; + String extensions = extensionsMessage(); + if (extensions.length() > 0) { + extensions = SEPARATOR + extensions; + } + + String message = String.format("n,%s,%sauth=Bearer %s%s%s%s", authzid, + SEPARATOR, tokenValue, extensions, SEPARATOR, SEPARATOR); + + return message.getBytes(StandardCharsets.UTF_8); + } + + /** + * 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; + } + + /** + * Validates that the given extensions conform to the standard. + * They should also not contain the reserve key name + * {@link OAuthBearerClientInitialResponse#AUTH_KEY} + * + * @param extensions + * optional extensions to validate + * @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 + * + * @see RFC 7628, + * Section 3.1 + */ + public static void validateExtensions(SaslExtensions extensions) throws SaslException { + if (extensions == null) { + return; + } + if (extensions.map().containsKey(OAuthBearerClientInitialResponse.AUTH_KEY)) { + throw new SaslException("Extension name " + + OAuthBearerClientInitialResponse.AUTH_KEY + " is invalid"); + } + + for (Map.Entry entry : extensions.map().entrySet()) { + String extensionName = entry.getKey(); + String extensionValue = entry.getValue(); + + if (!EXTENSION_KEY_PATTERN.matcher(extensionName).matches()) { + throw new SaslException("Extension name " + extensionName + " is invalid"); + } + if (!EXTENSION_VALUE_PATTERN.matcher(extensionValue).matches()) { + throw new SaslException("Extension value (" + extensionValue + ") for extension " + + extensionName + " is invalid"); + } + } + } + + /** + * Converts the SASLExtensions to an OAuth protocol-friendly string + */ + private String extensionsMessage() { + return Utils.mkString(saslExtensions.map(), "", "", "=", SEPARATOR); + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java new file mode 100644 index 000000000000..45365467e121 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import 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.SaslClient; +import javax.security.sasl.SaslClientFactory; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.exceptions.IllegalSaslStateException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +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}. Said handler can also optionally handle an instance of + * {@link SaslExtensionsCallback} to return any extensions 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}); + SaslExtensions extensions = retrieveCustomExtensions(); + + setState(State.RECEIVE_SERVER_FIRST_MESSAGE); + + return new OAuthBearerClientInitialResponse(callback.token().value(), extensions) + .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"); + } + return null; + } + + @Override + public void dispose() { + } + + private void setState(State state) { + LOG.debug("Setting SASL/{} client state to {}", OAUTHBEARER_MECHANISM, state); + this.state = state; + } + + private SaslExtensions retrieveCustomExtensions() throws SaslException { + SaslExtensionsCallback extensionsCallback = new SaslExtensionsCallback(); + try { + callbackHandler().handle(new Callback[] {extensionsCallback}); + } catch (UnsupportedCallbackException e) { + LOG.debug("Extensions callback is not supported by client callback handler {}, " + + "no extensions will be added", callbackHandler()); + } catch (Exception e) { + throw new SaslException("SASL extensions could not be obtained", e); + } + + return extensionsCallback.extensions(); + } + + 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 OAuthBearerSaslServer.mechanismNamesCompatibleWithPolicy(props); + } + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java new file mode 100644 index 000000000000..1b941e6941e7 --- /dev/null +++ b/hbase-common/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.token.OAuthBearerTokenUtil.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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java new file mode 100644 index 000000000000..5e7f91c3f429 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import 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.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.Utils; +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; + private SaslExtensions extensions; + + public OAuthBearerSaslServer(CallbackHandler callbackHandler) { + 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())); + } + 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(), + clientResponse.extensions()); + } catch (SaslAuthenticationException e) { + LOG.error("SASL authentication error: {}", e.getMessage()); + throw e; + } catch (Exception e) { + LOG.error("SASL server problem", e); + throw e; + } + } + + @Override + public String getAuthorizationID() { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return tokenForNegotiatedProperty.principalName(); + } + + @Override + public String getMechanismName() { + return OAUTHBEARER_MECHANISM; + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + if (NEGOTIATED_PROPERTY_KEY_TOKEN.equals(propName)) { + return tokenForNegotiatedProperty; + } + if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { + return tokenForNegotiatedProperty.lifetimeMs(); + } + return extensions.map().get(propName); + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(incoming, offset, offset + len); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(outgoing, offset, offset + len); + } + + @Override + public void dispose() { + complete = false; + tokenForNegotiatedProperty = null; + extensions = null; + } + + private byte[] process(String tokenValue, String authorizationId, SaslExtensions extensions) + 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())); + } + + Map validExtensions = processExtensions(token, extensions); + + tokenForNegotiatedProperty = token; + this.extensions = new SaslExtensions(validExtensions); + complete = true; + LOG.debug("Successfully authenticate User={}", token.principalName()); + return new byte[0]; + } + + private Map processExtensions(OAuthBearerToken token, SaslExtensions extensions) + throws SaslException { + OAuthBearerExtensionsValidatorCallback + extensionsCallback = new OAuthBearerExtensionsValidatorCallback(token, extensions); + try { + callbackHandler.handle(new Callback[] {extensionsCallback}); + } catch (UnsupportedCallbackException e) { + // backwards compatibility - no extensions will be added + } catch (IOException e) { + handleCallbackError(e); + } + if (!extensionsCallback.invalidExtensions().isEmpty()) { + String errorMessage = String.format("Authentication failed: %d extensions are invalid! " + + "They are: %s", extensionsCallback.invalidExtensions().size(), + Utils.mkString(extensionsCallback.invalidExtensions(), "", "", ": ", "; ")); + LOG.debug(errorMessage); + throw new SaslAuthenticationException(errorMessage); + } + + return extensionsCallback.validatedExtensions(); + } + + private static String jsonErrorResponse(String errorStatus, String errorScope, + String errorOpenIDConfiguration) { + String jsonErrorResponse = String.format("{\"status\":\"%s\"", errorStatus); + if (errorScope != null) { + jsonErrorResponse = String.format("%s, \"scope\":\"%s\"", jsonErrorResponse, errorScope); + } + if (errorOpenIDConfiguration != null) { + jsonErrorResponse = String.format("%s, \"openid-configuration\":\"%s\"", jsonErrorResponse, + errorOpenIDConfiguration); + } + jsonErrorResponse = String.format("%s}", jsonErrorResponse); + return jsonErrorResponse; + } + + 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 String[] mechanismNamesCompatibleWithPolicy(Map props) { + return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) + ? new String[] {} + : new String[] { OAUTHBEARER_MECHANISM}; + } + + 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 OAuthBearerSaslServer.mechanismNamesCompatibleWithPolicy(props); + } + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java new file mode 100644 index 000000000000..2d7aeed149a2 --- /dev/null +++ b/hbase-common/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.token.OAuthBearerTokenUtil.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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java new file mode 100644 index 000000000000..acd5b047e1c8 --- /dev/null +++ b/hbase-common/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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java new file mode 100644 index 000000000000..a45dd9cbb79d --- /dev/null +++ b/hbase-common/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).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).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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java new file mode 100644 index 000000000000..8421eb0bfc59 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java @@ -0,0 +1,199 @@ +/* + * 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.Calendar; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.Utils; +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); + Number expirationTimeSeconds = + DateUtils.ceiling(claims.getExpirationTime(), Calendar.SECOND).getTime() / 1000L; + if (expirationTimeSeconds == null) { + throw new OAuthBearerIllegalTokenException( + OAuthBearerValidationResult.newFailure("No expiration time in JWT")); + } + lifetime = convertClaimTimeInSecondsToMs(expirationTimeSeconds); + String principalName = claims.getSubject(); + if (Utils.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 (!Utils.isBlank(requiredAudience)) { + requiredClaims.add("aud"); + jwtClaimsSetBuilder.audience(requiredAudience); + } + + // Issuer + if (!Utils.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); + } + + private static long convertClaimTimeInSecondsToMs(Number claimValue) { + return Math.round(claimValue.doubleValue() * 1000); + } +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java new file mode 100644 index 000000000000..8896e9ddb1ce --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java @@ -0,0 +1,205 @@ +/* + * 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.token.OAuthBearerTokenUtil.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.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.Utils; +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)
  • + *
+ * + * It also recognizes {@link OAuthBearerExtensionsValidatorCallback} and validates + * every extension passed to it. + * + * 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 handler 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 if (callback instanceof OAuthBearerExtensionsValidatorCallback) { + OAuthBearerExtensionsValidatorCallback extensionsCallback = + (OAuthBearerExtensionsValidatorCallback) callback; + extensionsCallback.inputExtensions().map().forEach((extensionName, v) -> + extensionsCallback.valid(extensionName)); + } 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 = Utils.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 (Utils.isBlank(jwksFile) && Utils.isBlank(jwksUrl)) { + throw new RuntimeException("Failed to initialize JWKS db. " + + JWKS_FILE + " or " + JWKS_URL + " must be specified in the config."); + } + + if (!Utils.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-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java new file mode 100644 index 000000000000..d41962d5e67b --- /dev/null +++ b/hbase-common/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-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java new file mode 100644 index 000000000000..c96f714ccbc1 --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java @@ -0,0 +1,77 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.token; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Set; +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 OAUTHBEARER_MECHANISM = "OAUTHBEARER"; + 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) { + 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 0; + } + + @Override public String principalName() { + return null; + } + }; + subject.getPrivateCredentials().add(jwt); + return null; + } + }); + } +} 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..63c6c751502f --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/JwtTestUtils.java @@ -0,0 +1,116 @@ +/* + * 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.util.Date; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Public +public final class JwtTestUtils { + public static final long ONE_DAY = 24 * 60 * 60 * 1000L; + 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, + Date expirationTime, Date 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(issueTime) + .expirationTime(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 { + long now = new Date().getTime(); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(new Date(now + ONE_DAY)) + .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 { + long now = new Date().getTime(); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(new Date(now + ONE_DAY)) + .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 { + long now = new Date().getTime(); + JWSHeader jwsHeader = + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(rsaKey.getKeyID()) + .build(); + JWTClaimsSet payload = new JWTClaimsSet.Builder() + .subject(USER) + .expirationTime(new Date(now + ONE_DAY)) + .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/OAuthBearerExtensionsValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java new file mode 100644 index 000000000000..2105bfb06d2a --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java @@ -0,0 +1,94 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import java.util.HashMap; +import java.util.Map; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.junit.Test; + +public class OAuthBearerExtensionsValidatorCallbackTest { + private static final OAuthBearerToken TOKEN = new OAuthBearerTokenMock(); + + @Test + public void testValidatedExtensionsAreReturned() { + Map extensions = new HashMap<>(); + extensions.put("hello", "bye"); + + OAuthBearerExtensionsValidatorCallback callback = + new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); + + assertTrue(callback.validatedExtensions().isEmpty()); + assertTrue(callback.invalidExtensions().isEmpty()); + callback.valid("hello"); + assertFalse(callback.validatedExtensions().isEmpty()); + assertEquals("bye", callback.validatedExtensions().get("hello")); + assertTrue(callback.invalidExtensions().isEmpty()); + } + + @Test + public void testInvalidExtensionsAndErrorMessagesAreReturned() { + Map extensions = new HashMap<>(); + extensions.put("hello", "bye"); + + OAuthBearerExtensionsValidatorCallback callback = + new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); + + assertTrue(callback.validatedExtensions().isEmpty()); + assertTrue(callback.invalidExtensions().isEmpty()); + callback.error("hello", "error"); + assertFalse(callback.invalidExtensions().isEmpty()); + assertEquals("error", callback.invalidExtensions().get("hello")); + assertTrue(callback.validatedExtensions().isEmpty()); + } + + /** + * Extensions that are neither validated or invalidated must not be present in either maps + */ + @Test + public void testUnvalidatedExtensionsAreIgnored() { + Map extensions = new HashMap<>(); + extensions.put("valid", "valid"); + extensions.put("error", "error"); + extensions.put("nothing", "nothing"); + + OAuthBearerExtensionsValidatorCallback callback = + new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); + callback.error("error", "error"); + callback.valid("valid"); + + assertFalse(callback.validatedExtensions().containsKey("nothing")); + assertFalse(callback.invalidExtensions().containsKey("nothing")); + assertEquals("nothing", callback.ignoredExtensions().get("nothing")); + } + + @Test + public void testCannotValidateExtensionWhichWasNotGiven() { + Map extensions = new HashMap<>(); + extensions.put("hello", "bye"); + + OAuthBearerExtensionsValidatorCallback callback = + new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); + + assertThrows(IllegalArgumentException.class, () -> callback.valid("???")); + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java new file mode 100644 index 000000000000..539ce5f53794 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import org.junit.Test; + +public class OAuthBearerTokenCallbackTest { + 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-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java new file mode 100644 index 000000000000..9e6670b19d30 --- /dev/null +++ b/hbase-common/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-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java new file mode 100644 index 000000000000..dda0bbc70420 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import org.junit.Test; + +public class OAuthBearerValidatorCallbackTest { + 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-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..6caac9a2d969 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.junit.Test; + +public class OAuthBearerClientInitialResponseTest { + + /* + Test how a client would build a response + */ + @Test + public void testBuildClientResponseToBytes() throws Exception { + String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001nineteen=42\u0001\u0001"; + + Map extensions = new HashMap<>(); + extensions.put("nineteen", "42"); + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse("123.345.567", new SaslExtensions(extensions)); + + 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\u0001nineteen=42\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 testThrowsSaslExceptionOnInvalidExtensionKey() throws Exception { + Map extensions = new HashMap<>(); + extensions.put("19", "42"); // keys can only be a-z + assertThrows( + SaslException.class, () -> new OAuthBearerClientInitialResponse("123.345.567", + new SaslExtensions(extensions))); + } + + @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()); + assertEquals("valueA1, valueA2", response.extensions().map().get("propA")); + assertEquals("valueB", response.extensions().map().get("propB")); + } + + // 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()); + assertEquals("server.example.com", response.extensions().map().get("host")); + assertEquals("143", response.extensions().map().get("port")); + } + + @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()); + assertTrue(response.extensions().map().isEmpty()); + } + + @Test + public void testNoExtensionsFromTokenAndNullExtensions() throws Exception { + OAuthBearerClientInitialResponse response = + new OAuthBearerClientInitialResponse("token", null); + assertTrue(response.extensions().map().isEmpty()); + } + + @Test + public void testValidateNullExtensions() throws Exception { + OAuthBearerClientInitialResponse.validateExtensions(null); + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java new file mode 100644 index 000000000000..d4f38d911e9e --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; +import org.junit.Test; + +public class OAuthBearerSaslClientTest { + private static final Map TEST_PROPERTIES = new LinkedHashMap() { + { + put("One", "1"); + put("Two", "2"); + put("Three", "3"); + } + }; + private SaslExtensions testExtensions = new SaslExtensions(TEST_PROPERTIES); + private final String errorMessage = "Error as expected!"; + + public class ExtensionsCallbackHandler implements AuthenticateCallbackHandler { + private boolean configured = false; + private boolean toThrow; + + ExtensionsCallbackHandler(boolean toThrow) { + this.toThrow = toThrow; + } + + public boolean configured() { + return configured; + } + + @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 if (callback instanceof SaslExtensionsCallback) { + if (toThrow) { + throw new RuntimeException(errorMessage); + } else { + ((SaslExtensionsCallback) callback).extensions(testExtensions); + } + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + @Override + public void close() { + } + } + + @Test + public void testAttachesExtensionsToFirstClientMessage() throws Exception { + String expectedToken = new String( + new OAuthBearerClientInitialResponse("", testExtensions).toBytes(), StandardCharsets.UTF_8); + OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); + String message = new String(client.evaluateChallenge("".getBytes()), StandardCharsets.UTF_8); + assertEquals(expectedToken, message); + } + + @Test + public void testNoExtensionsDoesNotAttachAnythingToFirstClientMessage() throws Exception { + TEST_PROPERTIES.clear(); + testExtensions = new SaslExtensions(TEST_PROPERTIES); + String expectedToken = new String(new OAuthBearerClientInitialResponse("", + new SaslExtensions(TEST_PROPERTIES)).toBytes(), StandardCharsets.UTF_8); + OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); + + String message = new String(client.evaluateChallenge("".getBytes()), StandardCharsets.UTF_8); + + assertEquals(expectedToken, message); + } + + @Test + public void testWrapsExtensionsCallbackHandlingErrorInSaslExceptionInFirstClientMessage() { + OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(true)); + try { + client.evaluateChallenge("".getBytes()); + fail("Should have failed with " + SaslException.class.getName()); + } catch (SaslException e) { + // assert it has caught our expected exception + assertEquals(RuntimeException.class, e.getCause().getClass()); + assertEquals(errorMessage, e.getCause().getMessage()); + } + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java new file mode 100644 index 000000000000..b3df646ba7dc --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java @@ -0,0 +1,223 @@ +/* + * 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.assertNull; +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.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenMock; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerConfigException; +import org.apache.hadoop.hbase.security.oauthbearer.internals.knox.OAuthBearerSignedJwtValidatorCallbackHandler; +import org.junit.Before; +import org.junit.Test; + +public class OAuthBearerSaslServerTest { + private static final Configuration CONFIGS; + private static final AuthenticateCallbackHandler EXTENSIONS_VALIDATOR_CALLBACK_HANDLER; + static { + CONFIGS = new Configuration(); + EXTENSIONS_VALIDATOR_CALLBACK_HANDLER = new OAuthBearerSignedJwtValidatorCallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback) { + OAuthBearerValidatorCallback validationCallback = + (OAuthBearerValidatorCallback) callback; + validationCallback.token(new OAuthBearerTokenMock()); + } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { + OAuthBearerExtensionsValidatorCallback extensionsCallback = + (OAuthBearerExtensionsValidatorCallback) callback; + extensionsCallback.valid("firstKey"); + extensionsCallback.valid("secondKey"); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + }; + } + + 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)); + } + + /** + * SASL Extensions that are validated by the callback handler should be accessible through + * the {@code #getNegotiatedProperty()} method + */ + @Test + public void savesCustomExtensionAsNegotiatedProperty() throws Exception { + Map customExtensions = new HashMap<>(); + customExtensions.put("firstKey", "value1"); + customExtensions.put("secondKey", "value2"); + + byte[] nextChallenge = saslServer + .evaluateResponse(clientInitialResponse(null, false, customExtensions)); + + assertTrue("Next challenge is not empty", nextChallenge.length == 0); + assertEquals("value1", saslServer.getNegotiatedProperty("firstKey")); + assertEquals("value2", saslServer.getNegotiatedProperty("secondKey")); + } + + /** + * SASL Extensions that were not recognized (neither validated nor invalidated) + * by the callback handler must not be accessible through the {@code #getNegotiatedProperty()} + * method + */ + @Test + public void unrecognizedExtensionsAreNotSaved() throws Exception { + saslServer = new OAuthBearerSaslServer(EXTENSIONS_VALIDATOR_CALLBACK_HANDLER); + Map customExtensions = new HashMap<>(); + customExtensions.put("firstKey", "value1"); + customExtensions.put("secondKey", "value1"); + customExtensions.put("thirdKey", "value1"); + + byte[] nextChallenge = saslServer + .evaluateResponse(clientInitialResponse(null, false, customExtensions)); + + assertTrue("Next challenge is not empty", nextChallenge.length == 0); + assertNull("Extensions not recognized by the server must be ignored", + saslServer.getNegotiatedProperty("thirdKey")); + } + + /** + * If the callback handler handles the `OAuthBearerExtensionsValidatorCallback` + * and finds an invalid extension, SaslServer should throw an authentication exception + */ + @Test + public void throwsAuthenticationExceptionOnInvalidExtensions() { + OAuthBearerSignedJwtValidatorCallbackHandler invalidHandler = + new OAuthBearerSignedJwtValidatorCallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerValidatorCallback) { + OAuthBearerValidatorCallback validationCallback = + (OAuthBearerValidatorCallback) callback; + validationCallback.token(new OAuthBearerTokenMock()); + } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { + OAuthBearerExtensionsValidatorCallback extensionsCallback = + (OAuthBearerExtensionsValidatorCallback) callback; + extensionsCallback.error("firstKey", "is not valid"); + extensionsCallback.error("secondKey", "is not valid either"); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + }; + saslServer = new OAuthBearerSaslServer(invalidHandler); + Map customExtensions = new HashMap<>(); + customExtensions.put("firstKey", "value"); + customExtensions.put("secondKey", "value"); + + assertThrows(SaslAuthenticationException.class, + () -> saslServer.evaluateResponse(clientInitialResponse(null, false, customExtensions))); + } + + @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, + Collections.emptyMap())); + String challenge = new String(bytes, StandardCharsets.UTF_8); + assertEquals("{\"status\":\"invalid_token\"}", challenge); + } + + private byte[] clientInitialResponse(String authorizationId) + throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { + return clientInitialResponse(authorizationId, false); + } + + private byte[] clientInitialResponse(String authorizationId, boolean illegalToken) + throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { + return clientInitialResponse(authorizationId, false, Collections.emptyMap()); + } + + private byte[] clientInitialResponse(String authorizationId, boolean illegalToken, + Map customExtensions) + throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { + String compactSerialization = JWT; + String tokenValue = compactSerialization + (illegalToken ? "AB" : ""); + return new OAuthBearerClientInitialResponse(tokenValue, authorizationId, + new SaslExtensions(customExtensions)).toBytes(); + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java new file mode 100644 index 000000000000..9c8f42d28246 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.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.util.Calendar; +import java.util.Date; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.junit.Before; +import org.junit.Test; + +public class OAuthBearerSignedJwtTest { + 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"; + Date issuedAt = new Date(); + Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + 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(DateUtils.ceiling(issuedAt, Calendar.SECOND), + DateUtils.ceiling(Date.class.cast(jws.claims().get("iat")), Calendar.SECOND)); + assertEquals(DateUtils.ceiling(expirationTime, Calendar.SECOND), + DateUtils.ceiling(Date.class.cast(jws.claims().get("exp")), Calendar.SECOND)); + assertEquals(DateUtils.ceiling(expirationTime, Calendar.SECOND).getTime(), + jws.lifetimeMs()); + } + + @Test + public void missingPrincipal() throws JOSEException { + String subject = null; + Date issuedAt = new Date(); + Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + String validCompactSerialization = + compactSerialization(subject, issuedAt, expirationTime); + assertThrows(OAuthBearerIllegalTokenException.class, + () -> new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET).validate()); + } + + @Test + public void blankPrincipalName() throws JOSEException { + String subject = " "; + Date issuedAt = new Date(); + Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + 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, Date issuedAt, Date expirationTime) + throws JOSEException { + return JwtTestUtils.createSignedJwt(RSA_KEY, "me", subject, + expirationTime, issuedAt, "test-audience"); + } +} diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java new file mode 100644 index 000000000000..27bbc5ea6910 --- /dev/null +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java @@ -0,0 +1,154 @@ +/* + * 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.util.Date; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.hadoop.hbase.HBaseConfiguration; +import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.junit.Before; +import org.junit.Test; + +public class OAuthBearerSignedJwtValidatorCallbackHandlerTest { + 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 { + Date now = new Date(); + String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", + new Date(now.getTime() + JwtTestUtils.ONE_DAY), now, "test-aud"); + confirmFailsValidation(EMPTY_CONFIG, token); + } + + @Test + public void tooEarlyExpirationTime() throws JOSEException, UnsupportedCallbackException { + Date now = new Date(); + String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", + new Date(now.getTime() - JwtTestUtils.ONE_DAY), + new Date(now.getTime() - JwtTestUtils.ONE_DAY), + "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/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..4e529c679f8e --- /dev/null +++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java @@ -0,0 +1,106 @@ +/** + * 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 = ""; + + private static final byte[] FAMILY = Bytes.toBytes("d"); + + public JwtClientExample() { + setConf(HBaseConfiguration.create()); + } + + @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); + 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/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/provider/OAuthBearerSaslServerAuthenticationProvider.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java new file mode 100644 index 000000000000..9176c65a0b24 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/provider/OAuthBearerSaslServerAuthenticationProvider.java @@ -0,0 +1,99 @@ +/* + * 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.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 RuntimeException( + "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 RuntimeException("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/pom.xml b/pom.xml index e2a0b621bd5a..b44242351f74 100755 --- a/pom.xml +++ b/pom.xml @@ -1821,6 +1821,7 @@ 1.9 1.5.0-4 4.0.1 + 9.15 @@ -3204,6 +3205,10 @@ net.minidev json-smart + + com.nimbusds + nimbus-jose-jwt + org.slf4j slf4j-log4j12 @@ -3414,12 +3419,16 @@ hadoop-distcp ${hadoop-three.version} - org.apache.hadoop hadoop-hdfs-client ${hadoop-three.version} + + com.nimbusds + nimbus-jose-jwt + ${nimbusds.version} + From 4beb113342ecd742935c6edd10ca6b9bebae5fac Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Tue, 4 Jan 2022 12:26:06 +0100 Subject: [PATCH 02/15] HBASE-26553. Checkstyle and javadoc fixes --- .../SaslAuthenticationException.java | 3 +- .../hbase/security/auth/SaslExtensions.java | 13 ++++++--- ...AuthBearerExtensionsValidatorCallback.java | 13 +++++---- .../oauthbearer/OAuthBearerToken.java | 2 +- .../internals/OAuthBearerSaslClient.java | 2 +- .../internals/knox/OAuthBearerSignedJwt.java | 12 ++------ .../security/token/OAuthBearerTokenUtil.java | 1 - .../security/oauthbearer/JwtTestUtils.java | 23 +++++++-------- .../internals/OAuthBearerSaslClientTest.java | 8 +++-- .../internals/OAuthBearerSaslServerTest.java | 9 ++---- .../knox/OAuthBearerSignedJwtTest.java | 29 ++++++++++--------- ...SignedJwtValidatorCallbackHandlerTest.java | 12 ++++---- 12 files changed, 62 insertions(+), 65 deletions(-) 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 index f7537d99581b..9daa702782c1 100644 --- 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 @@ -32,7 +32,8 @@ * 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. + * security-critical information in the message that should not be leaked to unauthenticated + * clients. *

*/ @InterfaceAudience.Public diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java index c7683a9a0fc7..eb8839e37916 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java @@ -17,17 +17,18 @@ */ package org.apache.hadoop.hbase.security.auth; -import org.apache.yetus.audience.InterfaceAudience; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.apache.yetus.audience.InterfaceAudience; @InterfaceAudience.Public public class SaslExtensions { /** * An "empty" instance indicating no SASL extensions */ - public static final SaslExtensions NO_SASL_EXTENSIONS = new SaslExtensions(Collections.emptyMap()); + public static final SaslExtensions NO_SASL_EXTENSIONS = + new SaslExtensions(Collections.emptyMap()); private final Map extensionsMap; public SaslExtensions(Map extensionsMap) { @@ -43,8 +44,12 @@ public Map map() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } return extensionsMap.equals(((SaslExtensions) o).extensionsMap); } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java index a7e4d7dbb475..414068ffdef1 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -40,8 +40,9 @@ * Callback handlers should communicate other problems by raising an {@code IOException}. *

* The OAuth bearer token is provided in the callback for better context in extension validation. - * It is very important that token validation is done in its own {@link OAuthBearerValidatorCallback} - * irregardless of provided extensions, as they are inherently insecure. + * It is very important that token validation is done in its own + * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, as they are inherently + * insecure. */ @InterfaceAudience.Public public class OAuthBearerExtensionsValidatorCallback implements Callback { @@ -64,7 +65,7 @@ public OAuthBearerToken token() { /** * @return {@link SaslExtensions} consisting of the unvalidated extension names and values that - * were sent by the client + * were sent by the client */ public SaslExtensions inputExtensions() { return inputExtensions; @@ -72,7 +73,7 @@ public SaslExtensions inputExtensions() { /** * @return an unmodifiable {@link Map} consisting of the validated and recognized by the server - * extension names and values. + * extension names and values. */ public Map validatedExtensions() { return Collections.unmodifiableMap(validatedExtensions); @@ -80,7 +81,7 @@ public Map validatedExtensions() { /** * @return An immutable {@link Map} consisting of the name->error messages of extensions - * which failed validation + * which failed validation */ public Map invalidExtensions() { return Collections.unmodifiableMap(invalidExtensions); @@ -88,7 +89,7 @@ public Map invalidExtensions() { /** * @return An immutable {@link Map} consisting of the extensions that have neither been - * validated nor invalidated + * validated nor invalidated */ public Map ignoredExtensions() { return Collections.unmodifiableMap( 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 index f581aaa5ed0d..769bceea6181 100644 --- 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 @@ -28,7 +28,7 @@ *

* 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 #scope()}, {@link #lifetimeMs()}, etc.) declare no + * 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, diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java index 45365467e121..217fa125c961 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -51,7 +51,7 @@ * @see RFC 6750, * Section 2.1 * - * This class has been copy-and-pasted from Kafka codebase. + * This class has been copy-and-pasted from Kafka codebase. */ @InterfaceAudience.Public public class OAuthBearerSaslClient implements SaslClient { diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java index 8421eb0bfc59..b993e77a1704 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java @@ -32,12 +32,11 @@ import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; import com.nimbusds.jwt.proc.DefaultJWTProcessor; import java.text.ParseException; -import java.util.Calendar; +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.time.DateUtils; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.Utils; import org.apache.yetus.audience.InterfaceAudience; @@ -139,13 +138,12 @@ public OAuthBearerSignedJwt maxClockSkewSeconds(int value) { public OAuthBearerSignedJwt validate(){ try { this.claims = validateToken(compactSerialization); - Number expirationTimeSeconds = - DateUtils.ceiling(claims.getExpirationTime(), Calendar.SECOND).getTime() / 1000L; + Date expirationTimeSeconds = claims.getExpirationTime(); if (expirationTimeSeconds == null) { throw new OAuthBearerIllegalTokenException( OAuthBearerValidationResult.newFailure("No expiration time in JWT")); } - lifetime = convertClaimTimeInSecondsToMs(expirationTimeSeconds); + lifetime = expirationTimeSeconds.toInstant().toEpochMilli(); String principalName = claims.getSubject(); if (Utils.isBlank(principalName)) { throw new OAuthBearerIllegalTokenException(OAuthBearerValidationResult @@ -192,8 +190,4 @@ private JWTClaimsSet validateToken(String jwtToken) jwtProcessor.setJWSKeySelector(keySelector); return jwtProcessor.process(jwtToken, null); } - - private static long convertClaimTimeInSecondsToMs(Number claimValue) { - return Math.round(claimValue.doubleValue() * 1000); - } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java index c96f714ccbc1..e4e8e0b3441e 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java @@ -20,7 +20,6 @@ import java.security.AccessController; import java.security.PrivilegedAction; -import java.util.Set; import javax.security.auth.Subject; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; 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 index 63c6c751502f..5e66aae56402 100644 --- 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 @@ -26,12 +26,12 @@ import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import java.util.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; import org.apache.yetus.audience.InterfaceAudience; @InterfaceAudience.Public public final class JwtTestUtils { - public static final long ONE_DAY = 24 * 60 * 60 * 1000L; public static final String USER = "user"; public static RSAKey generateRSAKey() throws JOSEException { @@ -40,7 +40,7 @@ public static RSAKey generateRSAKey() throws JOSEException { } public static String createSignedJwt(RSAKey rsaKey, String issuer, String subject, - Date expirationTime, Date issueTime, String audience) + LocalDate expirationTime, LocalDate issueTime, String audience) throws JOSEException { JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) @@ -50,8 +50,8 @@ public static String createSignedJwt(RSAKey rsaKey, String issuer, String subjec JWTClaimsSet payload = new JWTClaimsSet.Builder() .issuer(issuer) .subject(subject) - .issueTime(issueTime) - .expirationTime(expirationTime) + .issueTime(java.sql.Date.valueOf(issueTime)) + .expirationTime(java.sql.Date.valueOf(expirationTime)) .audience(audience) .build(); SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); @@ -60,7 +60,7 @@ public static String createSignedJwt(RSAKey rsaKey, String issuer, String subjec } public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { - long now = new Date().getTime(); + LocalDateTime now = LocalDateTime.now(); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) @@ -68,7 +68,7 @@ public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { .build(); JWTClaimsSet payload = new JWTClaimsSet.Builder() .subject(USER) - .expirationTime(new Date(now + ONE_DAY)) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) .build(); SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); signedJwt.sign(new RSASSASigner(rsaKey)); @@ -76,7 +76,7 @@ public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { } public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) throws JOSEException { - long now = new Date().getTime(); + LocalDateTime now = LocalDateTime.now(); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) @@ -84,7 +84,7 @@ public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) thro .build(); JWTClaimsSet payload = new JWTClaimsSet.Builder() .subject(USER) - .expirationTime(new Date(now + ONE_DAY)) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) .audience(aud) .build(); SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); @@ -93,7 +93,7 @@ public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) thro } public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws JOSEException { - long now = new Date().getTime(); + LocalDateTime now = LocalDateTime.now(); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) @@ -101,7 +101,7 @@ public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws .build(); JWTClaimsSet payload = new JWTClaimsSet.Builder() .subject(USER) - .expirationTime(new Date(now + ONE_DAY)) + .expirationTime(java.sql.Timestamp.valueOf(now.plusDays(1))) .issuer(iss) .build(); SignedJWT signedJwt = new SignedJWT(jwsHeader, payload); @@ -109,7 +109,6 @@ public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws return signedJwt.serialize(); } - private JwtTestUtils() { // empty } diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java index d4f38d911e9e..ae1a28e2110b 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -94,7 +94,8 @@ public void testAttachesExtensionsToFirstClientMessage() throws Exception { String expectedToken = new String( new OAuthBearerClientInitialResponse("", testExtensions).toBytes(), StandardCharsets.UTF_8); OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); - String message = new String(client.evaluateChallenge("".getBytes()), StandardCharsets.UTF_8); + String message = new String(client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); assertEquals(expectedToken, message); } @@ -106,7 +107,8 @@ public void testNoExtensionsDoesNotAttachAnythingToFirstClientMessage() throws E new SaslExtensions(TEST_PROPERTIES)).toBytes(), StandardCharsets.UTF_8); OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); - String message = new String(client.evaluateChallenge("".getBytes()), StandardCharsets.UTF_8); + String message = new String(client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); assertEquals(expectedToken, message); } @@ -115,7 +117,7 @@ public void testNoExtensionsDoesNotAttachAnythingToFirstClientMessage() throws E public void testWrapsExtensionsCallbackHandlingErrorInSaslExceptionInFirstClientMessage() { OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(true)); try { - client.evaluateChallenge("".getBytes()); + client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)); fail("Should have failed with " + SaslException.class.getName()); } catch (SaslException e) { // assert it has caught our expected exception diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java index b3df646ba7dc..869b90314511 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java @@ -203,18 +203,13 @@ public void illegalToken() throws Exception { } private byte[] clientInitialResponse(String authorizationId) - throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { - return clientInitialResponse(authorizationId, false); - } - - private byte[] clientInitialResponse(String authorizationId, boolean illegalToken) - throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { + throws OAuthBearerConfigException, IOException { return clientInitialResponse(authorizationId, false, Collections.emptyMap()); } private byte[] clientInitialResponse(String authorizationId, boolean illegalToken, Map customExtensions) - throws OAuthBearerConfigException, IOException, UnsupportedCallbackException { + throws OAuthBearerConfigException, IOException { String compactSerialization = JWT; String tokenValue = compactSerialization + (illegalToken ? "AB" : ""); return new OAuthBearerClientInitialResponse(tokenValue, authorizationId, diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java index 9c8f42d28246..59d347f2da49 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java @@ -22,9 +22,9 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; -import java.util.Calendar; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Date; -import org.apache.commons.lang3.time.DateUtils; import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; import org.junit.Before; import org.junit.Test; @@ -42,27 +42,28 @@ public void before() throws JOSEException { @Test public void validCompactSerialization() throws JOSEException { String subject = "foo"; - Date issuedAt = new Date(); - Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + + LocalDate issuedAt = LocalDate.now(); + LocalDate expirationTime = issuedAt.plusDays(1); 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(DateUtils.ceiling(issuedAt, Calendar.SECOND), - DateUtils.ceiling(Date.class.cast(jws.claims().get("iat")), Calendar.SECOND)); - assertEquals(DateUtils.ceiling(expirationTime, Calendar.SECOND), - DateUtils.ceiling(Date.class.cast(jws.claims().get("exp")), Calendar.SECOND)); - assertEquals(DateUtils.ceiling(expirationTime, Calendar.SECOND).getTime(), + 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; - Date issuedAt = new Date(); - Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + LocalDate issuedAt = LocalDate.now(); + LocalDate expirationTime = issuedAt.plusDays(1); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); assertThrows(OAuthBearerIllegalTokenException.class, @@ -72,8 +73,8 @@ public void missingPrincipal() throws JOSEException { @Test public void blankPrincipalName() throws JOSEException { String subject = " "; - Date issuedAt = new Date(); - Date expirationTime = new Date(issuedAt.getTime() + 60 * 60); + LocalDate issuedAt = LocalDate.now(); + LocalDate expirationTime = issuedAt.plusDays(1); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); assertThrows(OAuthBearerIllegalTokenException.class, @@ -101,7 +102,7 @@ public void badIssuer() throws JOSEException { } - private String compactSerialization(String subject, Date issuedAt, Date expirationTime) + 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-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java index 27bbc5ea6910..09469aa3d944 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java @@ -25,7 +25,7 @@ import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; -import java.util.Date; +import java.time.LocalDate; import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; import org.apache.hadoop.hbase.HBaseConfiguration; @@ -65,18 +65,18 @@ public void validToken() throws JOSEException, UnsupportedCallbackException { @Test public void missingPrincipal() throws UnsupportedCallbackException, JOSEException { - Date now = new Date(); + LocalDate now = LocalDate.now(); String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", - new Date(now.getTime() + JwtTestUtils.ONE_DAY), now, "test-aud"); + now.plusDays(1), now, "test-aud"); confirmFailsValidation(EMPTY_CONFIG, token); } @Test public void tooEarlyExpirationTime() throws JOSEException, UnsupportedCallbackException { - Date now = new Date(); + LocalDate now = LocalDate.now(); String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", - new Date(now.getTime() - JwtTestUtils.ONE_DAY), - new Date(now.getTime() - JwtTestUtils.ONE_DAY), + now.minusDays(1), + now.minusDays(1), "test-aud"); confirmFailsValidation(EMPTY_CONFIG, token); } From e8e41d9f1f3b142f0944432ec33e47083a7605db Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Tue, 4 Jan 2022 15:23:51 +0100 Subject: [PATCH 03/15] HBASE-26553. Address taklwu's comments --- .../OAuthBearerSaslClientCallbackHandlerTest.java | 2 +- .../hbase/exceptions/IllegalSaslStateException.java | 1 - .../security/auth/AuthenticateCallbackHandler.java | 5 ----- .../OAuthBearerExtensionsValidatorCallback.java | 2 +- .../{Utils.java => OAuthBearerStringUtils.java} | 13 ++----------- .../oauthbearer/OAuthBearerTokenCallback.java | 2 +- .../internals/OAuthBearerClientInitialResponse.java | 6 +++--- .../internals/OAuthBearerSaslServer.java | 6 +++--- .../internals/knox/OAuthBearerSignedJwt.java | 8 ++++---- ...AuthBearerSignedJwtValidatorCallbackHandler.java | 10 +++++----- .../oauthbearer/OAuthBearerTokenCallbackTest.java | 11 +++++++++++ .../OAuthBearerValidatorCallbackTest.java | 1 + .../internals/OAuthBearerSaslClientTest.java | 4 ---- .../hadoop/hbase/util/ClassLoaderTestHelper.java | 2 -- .../hbase/jwt/client/example/JwtClientExample.java | 11 ++++++++--- 15 files changed, 40 insertions(+), 44 deletions(-) rename hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/{Utils.java => OAuthBearerStringUtils.java} (89%) 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 index a780052767db..cce0805bb6ff 100644 --- 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 @@ -68,7 +68,7 @@ public void testWithZeroTokens() { assertEquals(IOException.class, e.getCause().getClass()); } - @Test() + @Test public void testWithPotentiallyMultipleTokens() throws Exception { OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = createCallbackHandler(); 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 index a18904603f5d..987c32fcec49 100644 --- 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 @@ -36,5 +36,4 @@ public IllegalSaslStateException(String message) { public IllegalSaslStateException(String message, Throwable cause) { super(message, cause); } - } 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 index 6b966bbae19f..24db41e99ebf 100644 --- 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 @@ -45,9 +45,4 @@ public interface AuthenticateCallbackHandler extends CallbackHandler { */ default void configure( Configuration configs, String saslMechanism, Map saslProps) {} - - /** - * Closes this instance. - */ - default void close() {} } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java index 414068ffdef1..e37aa8d4774f 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer; -import static org.apache.hadoop.hbase.security.oauthbearer.Utils.subtractMap; +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java similarity index 89% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java rename to hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java index 097046963aec..0b3c10a8b0e9 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/Utils.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java @@ -23,7 +23,7 @@ import org.apache.yetus.audience.InterfaceAudience; @InterfaceAudience.Public -public final class Utils { +public final class OAuthBearerStringUtils { /** * Converts a {@code Map} class into a string, concatenating keys and values * Example: @@ -76,16 +76,7 @@ public static Map subtractMap(Map minuend .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - /** - * Checks if a string is null, empty or whitespace only. - * @param str a string to be checked - * @return true if the string is null, empty or whitespace only; otherwise, return false. - */ - public static boolean isBlank(String str) { - return str == null || str.trim().isEmpty(); - } - - private Utils() { + private OAuthBearerStringUtils() { // empty } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java index 51a76b509fa7..2f8e982c6f4e 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -30,7 +30,7 @@ * 2.0 Authorization Framework. 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 + * 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 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 index 4bc4ebf01761..4b5393b2dbb7 100644 --- 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 @@ -24,7 +24,7 @@ import java.util.regex.Pattern; import javax.security.sasl.SaslException; import org.apache.hadoop.hbase.security.auth.SaslExtensions; -import org.apache.hadoop.hbase.security.oauthbearer.Utils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.yetus.audience.InterfaceAudience; /** @@ -64,7 +64,7 @@ public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { String authzid = matcher.group("authzid"); this.authorizationId = authzid == null ? "" : authzid; String kvPairs = matcher.group("kvpairs"); - Map properties = Utils.parseMap(kvPairs, "=", SEPARATOR); + Map properties = OAuthBearerStringUtils.parseMap(kvPairs, "=", SEPARATOR); String auth = properties.get(AUTH_KEY); if (auth == null) { throw new SaslException("Invalid OAUTHBEARER client first message: 'auth' not specified"); @@ -207,6 +207,6 @@ public static void validateExtensions(SaslExtensions extensions) throws SaslExce * Converts the SASLExtensions to an OAuth protocol-friendly string */ private String extensionsMessage() { - return Utils.mkString(saslExtensions.map(), "", "", "=", SEPARATOR); + return OAuthBearerStringUtils.mkString(saslExtensions.map(), "", "", "=", SEPARATOR); } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java index 5e7f91c3f429..216eaea0e086 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java @@ -36,7 +36,7 @@ import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; -import org.apache.hadoop.hbase.security.oauthbearer.Utils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,7 +103,7 @@ public byte[] evaluateResponse(byte[] response) return process(clientResponse.tokenValue(), clientResponse.authorizationId(), clientResponse.extensions()); } catch (SaslAuthenticationException e) { - LOG.error("SASL authentication error: {}", e.getMessage()); + LOG.error("SASL authentication error", e); throw e; } catch (Exception e) { LOG.error("SASL server problem", e); @@ -215,7 +215,7 @@ private Map processExtensions(OAuthBearerToken token, SaslExtens if (!extensionsCallback.invalidExtensions().isEmpty()) { String errorMessage = String.format("Authentication failed: %d extensions are invalid! " + "They are: %s", extensionsCallback.invalidExtensions().size(), - Utils.mkString(extensionsCallback.invalidExtensions(), "", "", ": ", "; ")); + OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), "", "", ": ", "; ")); LOG.debug(errorMessage); throw new SaslAuthenticationException(errorMessage); } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java index b993e77a1704..f17457d66edc 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java @@ -37,8 +37,8 @@ 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.hadoop.hbase.security.oauthbearer.Utils; import org.apache.yetus.audience.InterfaceAudience; /** @@ -145,7 +145,7 @@ public OAuthBearerSignedJwt validate(){ } lifetime = expirationTimeSeconds.toInstant().toEpochMilli(); String principalName = claims.getSubject(); - if (Utils.isBlank(principalName)) { + if (StringUtils.isBlank(principalName)) { throw new OAuthBearerIllegalTokenException(OAuthBearerValidationResult .newFailure("No principal name in JWT claim")); } @@ -165,13 +165,13 @@ private JWTClaimsSet validateToken(String jwtToken) JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder(); // Audience - if (!Utils.isBlank(requiredAudience)) { + if (!StringUtils.isBlank(requiredAudience)) { requiredClaims.add("aud"); jwtClaimsSetBuilder.audience(requiredAudience); } // Issuer - if (!Utils.isBlank(requiredIssuer)) { + if (!StringUtils.isBlank(requiredIssuer)) { requiredClaims.add("iss"); jwtClaimsSetBuilder.issuer(requiredIssuer); } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java index 8896e9ddb1ce..e4f8e9d4a1a4 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java @@ -27,11 +27,11 @@ 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.OAuthBearerExtensionsValidatorCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; -import org.apache.hadoop.hbase.security.oauthbearer.Utils; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,7 +91,7 @@ public class OAuthBearerSignedJwtValidatorCallbackHandler implements Authenticat public void handle(Callback[] callbacks) throws UnsupportedCallbackException { if (!configured) { throw new RuntimeException( - "OAuthBearerSignedJwtValidatorCallbackHandler handler be configured first."); + "OAuthBearerSignedJwtValidatorCallbackHandler must be configured first."); } for (Callback callback : callbacks) { @@ -171,7 +171,7 @@ private int allowableClockSkewSeconds() { ALLOWABLE_CLOCK_SKEW_SECONDS_OPTION); int allowableClockSkewSeconds = 0; try { - allowableClockSkewSeconds = Utils.isBlank(allowableClockSkewSecondsValue) + allowableClockSkewSeconds = StringUtils.isBlank(allowableClockSkewSecondsValue) ? 0 : Integer.parseInt(allowableClockSkewSecondsValue.trim()); } catch (NumberFormatException e) { throw new OAuthBearerConfigException(e.getMessage(), e); @@ -188,12 +188,12 @@ private void loadJwkSet() throws IOException, ParseException { String jwksFile = hBaseConfiguration.get(JWKS_FILE); String jwksUrl = hBaseConfiguration.get(JWKS_URL); - if (Utils.isBlank(jwksFile) && Utils.isBlank(jwksUrl)) { + 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 (!Utils.isBlank(jwksFile)) { + if (!StringUtils.isBlank(jwksFile)) { this.jwkSet = JWKSet.load(new File(jwksFile)); LOG.debug("JWKS db initialized from file: {}", jwksFile); return; diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java index 539ce5f53794..06ce6577ce5d 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java @@ -20,9 +20,20 @@ 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() { diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java index dda0bbc70420..38ee2f11e547 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertSame; import org.junit.Test; + public class OAuthBearerValidatorCallbackTest { private static final OAuthBearerToken TOKEN = new OAuthBearerToken() { @Override diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java index ae1a28e2110b..c0d60e0129b2 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -83,10 +83,6 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { } } } - - @Override - public void close() { - } } @Test diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java index fd771c722b88..977040eb538b 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/ClassLoaderTestHelper.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; - import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; @@ -35,7 +34,6 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.slf4j.Logger; diff --git a/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java b/hbase-examples/src/main/java/org/apache/hadoop/hbase/jwt/client/example/JwtClientExample.java index 4e529c679f8e..78817e0347cb 100644 --- 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 @@ -50,12 +50,17 @@ @InterfaceAudience.Private public class JwtClientExample extends Configured implements Tool { private static final Logger LOG = LoggerFactory.getLogger(JwtClientExample.class); - private static final String jwt = ""; + private static final String JWT_TOKEN = ""; private static final byte[] FAMILY = Bytes.toBytes("d"); public JwtClientExample() { - setConf(HBaseConfiguration.create()); + 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 { @@ -66,7 +71,7 @@ public JwtClientExample() { UserProvider provider = UserProvider.instantiate(conf); User user = provider.getCurrent(); - OAuthBearerTokenUtil.addTokenForUser(user, jwt); + OAuthBearerTokenUtil.addTokenForUser(user, JWT_TOKEN); LOG.info("JWT token added"); try (final Connection conn = ConnectionFactory.createConnection(conf, user)) { From 05596593e6c99744971338cecae20c857e5f89bd Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Tue, 4 Jan 2022 17:56:02 +0100 Subject: [PATCH 04/15] HBASE-26553. Address meszibalu's comments --- ...earerSaslClientAuthenticationProvider.java | 13 +++++----- ...AuthBearerExtensionsValidatorCallback.java | 3 ++- .../oauthbearer/OAuthBearerTokenCallback.java | 4 +-- .../OAuthBearerValidatorCallback.java | 5 ++-- .../internals/OAuthBearerSaslServer.java | 25 ++++++++++--------- .../OAuthBearerIllegalTokenException.java | 4 +-- .../security/oauthbearer/JwtTestUtils.java | 8 +++--- .../knox/OAuthBearerSignedJwtTest.java | 9 ++++--- ...SignedJwtValidatorCallbackHandlerTest.java | 6 +++-- ...earerSaslServerAuthenticationProvider.java | 2 +- 10 files changed, 44 insertions(+), 35 deletions(-) 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 index 466bcec0cd35..b1a8d5b262ed 100644 --- 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 @@ -82,7 +82,7 @@ public static class OAuthBearerSaslClientCallbackHandler implements Authenticate @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { if (!configured) { - throw new RuntimeException( + throw new IllegalStateException( "OAuthBearerSaslClientCallbackHandler handler must be configured first."); } @@ -131,11 +131,12 @@ public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { } }); sortedByLifetime.addAll(privateCredentials); - LOG.warn("Found {} OAuth Bearer tokens in Subject's private credentials; " - + "the oldest expires at {}, will use the newest, which expires at {}", - sortedByLifetime.size(), - new Date(sortedByLifetime.first().lifetimeMs()), - new Date(sortedByLifetime.last().lifetimeMs())); + 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(), new Date(sortedByLifetime.first().lifetimeMs()), + new Date(sortedByLifetime.last().lifetimeMs())); + } callback.token(sortedByLifetime.last()); } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java index e37aa8d4774f..05bdb62ad76a 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Objects; import javax.security.auth.callback.Callback; +import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.yetus.audience.InterfaceAudience; @@ -116,7 +117,7 @@ public void valid(String extensionName) { * error message describing why the validation failed */ public void error(String invalidExtensionName, String errorMessage) { - if (Objects.requireNonNull(invalidExtensionName).isEmpty()) { + if (StringUtils.isEmpty(invalidExtensionName)) { throw new IllegalArgumentException("extension name must not be empty"); } this.invalidExtensions.put(invalidExtensionName, errorMessage); diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java index 2f8e982c6f4e..6e9eeabb8652 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -17,8 +17,8 @@ */ 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; /** @@ -111,7 +111,7 @@ public void token(OAuthBearerToken token) { * the optional error URI to set */ public void error(String errorCode, String errorDescription, String errorUri) { - if (Objects.requireNonNull(errorCode).isEmpty()) { + if (StringUtils.isEmpty(errorCode)) { throw new IllegalArgumentException("error code must not be empty"); } this.errorCode = errorCode; diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java index a0469d121052..ec4b3c2a329e 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java @@ -19,6 +19,7 @@ import java.util.Objects; import javax.security.auth.callback.Callback; +import org.apache.commons.lang3.StringUtils; import org.apache.yetus.audience.InterfaceAudience; /** @@ -54,7 +55,7 @@ public class OAuthBearerValidatorCallback implements Callback { * the mandatory/non-blank token value */ public OAuthBearerValidatorCallback(String tokenValue) { - if (Objects.requireNonNull(tokenValue).isEmpty()) { + if (StringUtils.isEmpty(tokenValue)) { throw new IllegalArgumentException("token value must not be empty"); } this.tokenValue = tokenValue; @@ -144,7 +145,7 @@ public void token(OAuthBearerToken token) { * the optional error openid-configuration value to set */ public void error(String errorStatus, String errorScope, String errorOpenIDConfiguration) { - if (Objects.requireNonNull(errorStatus).isEmpty()) { + if (StringUtils.isEmpty(errorStatus)) { throw new IllegalArgumentException("error status must not be empty"); } this.errorStatus = errorStatus; diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java index 216eaea0e086..1b330a6b92de 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java @@ -18,11 +18,11 @@ package org.apache.hadoop.hbase.security.oauthbearer.internals; import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.shaded.json.JSONObject; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; -import java.util.Objects; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; @@ -30,13 +30,14 @@ import javax.security.sasl.SaslException; import javax.security.sasl.SaslServer; import javax.security.sasl.SaslServerFactory; +import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,7 +66,7 @@ public class OAuthBearerSaslServer implements SaslServer { private SaslExtensions extensions; public OAuthBearerSaslServer(CallbackHandler callbackHandler) { - if (!(Objects.requireNonNull(callbackHandler) instanceof AuthenticateCallbackHandler)) { + if (!(callbackHandler instanceof AuthenticateCallbackHandler)) { throw new IllegalArgumentException( String.format("Callback handler must be castable to %s: %s", AuthenticateCallbackHandler.class.getName(), callbackHandler.getClass().getName())); @@ -215,7 +216,8 @@ private Map processExtensions(OAuthBearerToken token, SaslExtens if (!extensionsCallback.invalidExtensions().isEmpty()) { String errorMessage = String.format("Authentication failed: %d extensions are invalid! " + "They are: %s", extensionsCallback.invalidExtensions().size(), - OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), "", "", ": ", "; ")); + OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), + "", "", ": ", "; ")); LOG.debug(errorMessage); throw new SaslAuthenticationException(errorMessage); } @@ -225,16 +227,15 @@ private Map processExtensions(OAuthBearerToken token, SaslExtens private static String jsonErrorResponse(String errorStatus, String errorScope, String errorOpenIDConfiguration) { - String jsonErrorResponse = String.format("{\"status\":\"%s\"", errorStatus); - if (errorScope != null) { - jsonErrorResponse = String.format("%s, \"scope\":\"%s\"", jsonErrorResponse, errorScope); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("status", errorStatus); + if (!StringUtils.isBlank(errorScope)) { + jsonObject.put("scope", errorScope); } - if (errorOpenIDConfiguration != null) { - jsonErrorResponse = String.format("%s, \"openid-configuration\":\"%s\"", jsonErrorResponse, - errorOpenIDConfiguration); + if (!StringUtils.isBlank(errorOpenIDConfiguration)) { + jsonObject.put("openid-configuration", errorOpenIDConfiguration); } - jsonErrorResponse = String.format("%s}", jsonErrorResponse); - return jsonErrorResponse; + return jsonObject.toJSONString(); } private void handleCallbackError(Exception e) throws SaslException { diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java index a45dd9cbb79d..09aa28c57ee4 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java @@ -37,7 +37,7 @@ public class OAuthBearerIllegalTokenException extends RuntimeException { * failure */ public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason) { - super(Objects.requireNonNull(reason).failureDescription()); + super(Objects.requireNonNull(reason, "Reason cannot be null").failureDescription()); if (reason.success()) { throw new IllegalArgumentException( "The reason indicates success; it must instead indicate failure"); @@ -46,7 +46,7 @@ public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason) { } public OAuthBearerIllegalTokenException(OAuthBearerValidationResult reason, Throwable t) { - super(Objects.requireNonNull(reason).failureDescription(), 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"); 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 index 5e66aae56402..39b4330425d9 100644 --- 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 @@ -28,10 +28,12 @@ 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 { @@ -60,7 +62,7 @@ public static String createSignedJwt(RSAKey rsaKey, String issuer, String subjec } public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = LocalDateTime.now(ZONE_ID); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) @@ -76,7 +78,7 @@ public static String createSignedJwt(RSAKey rsaKey) throws JOSEException { } public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) throws JOSEException { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = LocalDateTime.now(ZONE_ID); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) @@ -93,7 +95,7 @@ public static String createSignedJwtWithAudience(RSAKey rsaKey, String aud) thro } public static String createSignedJwtWithIssuer(RSAKey rsaKey, String iss) throws JOSEException { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = LocalDateTime.now(ZONE_ID); JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java index 59d347f2da49..deaf1a8cecea 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java @@ -30,6 +30,8 @@ import org.junit.Test; public class OAuthBearerSignedJwtTest { + private final static ZoneId ZONE_ID = ZoneId.of("America/Los_Angeles"); + private JWKSet JWK_SET; private RSAKey RSA_KEY; @@ -43,7 +45,7 @@ public void before() throws JOSEException { public void validCompactSerialization() throws JOSEException { String subject = "foo"; - LocalDate issuedAt = LocalDate.now(); + LocalDate issuedAt = LocalDate.now(ZONE_ID); LocalDate expirationTime = issuedAt.plusDays(1); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); @@ -62,7 +64,7 @@ public void validCompactSerialization() throws JOSEException { @Test public void missingPrincipal() throws JOSEException { String subject = null; - LocalDate issuedAt = LocalDate.now(); + LocalDate issuedAt = LocalDate.now(ZONE_ID); LocalDate expirationTime = issuedAt.plusDays(1); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); @@ -73,7 +75,7 @@ public void missingPrincipal() throws JOSEException { @Test public void blankPrincipalName() throws JOSEException { String subject = " "; - LocalDate issuedAt = LocalDate.now(); + LocalDate issuedAt = LocalDate.now(ZONE_ID); LocalDate expirationTime = issuedAt.plusDays(1); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); @@ -101,7 +103,6 @@ public void badIssuer() throws JOSEException { .validate()); } - private String compactSerialization(String subject, LocalDate issuedAt, LocalDate expirationTime) throws JOSEException { return JwtTestUtils.createSignedJwt(RSA_KEY, "me", subject, diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java index 09469aa3d944..a4144a197f5e 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java @@ -26,6 +26,7 @@ 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.HBaseConfiguration; @@ -35,6 +36,7 @@ import org.junit.Test; public class OAuthBearerSignedJwtValidatorCallbackHandlerTest { + 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 { @@ -65,7 +67,7 @@ public void validToken() throws JOSEException, UnsupportedCallbackException { @Test public void missingPrincipal() throws UnsupportedCallbackException, JOSEException { - LocalDate now = LocalDate.now(); + LocalDate now = LocalDate.now(ZONE_ID); String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", now.plusDays(1), now, "test-aud"); confirmFailsValidation(EMPTY_CONFIG, token); @@ -73,7 +75,7 @@ public void missingPrincipal() @Test public void tooEarlyExpirationTime() throws JOSEException, UnsupportedCallbackException { - LocalDate now = LocalDate.now(); + LocalDate now = LocalDate.now(ZONE_ID); String token = JwtTestUtils.createSignedJwt(RSA_KEY, "me", "", now.minusDays(1), now.minusDays(1), 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 index 9176c65a0b24..60eb743a9294 100644 --- 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 @@ -58,7 +58,7 @@ public class OAuthBearerSaslServerAuthenticationProvider throws IOException { if (!initialized) { - throw new RuntimeException( + throw new IllegalStateException( "OAuthBearerSaslServerAuthenticationProvider must be initialized first."); } From ef3563c7e544c2be784a32d0158ac9743b6df414 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Wed, 5 Jan 2022 17:44:53 +0100 Subject: [PATCH 05/15] HBASE-26553. Added Category/ClassRule to test classes --- .../OAuthBearerSaslClientCallbackHandlerTest.java | 10 ++++++++++ .../oauthbearer/internals/OAuthBearerSaslClient.java | 3 +-- .../OAuthBearerExtensionsValidatorCallbackTest.java | 10 ++++++++++ .../oauthbearer/OAuthBearerValidatorCallbackTest.java | 11 ++++++++++- .../OAuthBearerClientInitialResponseTest.java | 9 +++++++++ .../internals/OAuthBearerSaslClientTest.java | 10 ++++++++++ .../internals/OAuthBearerSaslServerTest.java | 10 ++++++++++ .../internals/knox/OAuthBearerSignedJwtTest.java | 9 +++++++++ ...thBearerSignedJwtValidatorCallbackHandlerTest.java | 10 ++++++++++ 9 files changed, 79 insertions(+), 3 deletions(-) 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 index cce0805bb6ff..ed229d28af48 100644 --- 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 @@ -28,12 +28,22 @@ 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.security.token.OAuthBearerTokenUtil; +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 diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java index 217fa125c961..d76fe845da85 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -48,8 +48,7 @@ * {@link SaslExtensionsCallback} to return any extensions generated by the * {@code login()} event on the {@code LoginContext}. * - * @see RFC 6750, - * Section 2.1 + * @see RFC 6750 Section 2.1 * * This class has been copy-and-pasted from Kafka codebase. */ diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java index 2105bfb06d2a..3488a2412f3d 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java @@ -23,10 +23,20 @@ import static org.junit.Assert.assertTrue; import java.util.HashMap; import java.util.Map; +import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.security.auth.SaslExtensions; +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 OAuthBearerExtensionsValidatorCallbackTest { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(OAuthBearerExtensionsValidatorCallbackTest.class); + private static final OAuthBearerToken TOKEN = new OAuthBearerTokenMock(); @Test diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java index 38ee2f11e547..8be3cfcaf4a4 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java @@ -20,10 +20,19 @@ 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() { 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 index 6caac9a2d969..54e266eaeda8 100644 --- 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 @@ -24,10 +24,19 @@ import java.util.HashMap; import java.util.Map; import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.security.auth.SaslExtensions; +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 diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java index c0d60e0129b2..7bcf55cbe167 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -25,14 +25,24 @@ import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.sasl.SaslException; +import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; 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); + private static final Map TEST_PROPERTIES = new LinkedHashMap() { { put("One", "1"); diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java index 869b90314511..d1a8dc5f3597 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java @@ -34,6 +34,7 @@ import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; 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.auth.AuthenticateCallbackHandler; import org.apache.hadoop.hbase.security.auth.SaslExtensions; @@ -44,10 +45,19 @@ import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; 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; private static final AuthenticateCallbackHandler EXTENSIONS_VALIDATOR_CALLBACK_HANDLER; static { diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java index deaf1a8cecea..f4e9bfac7692 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java @@ -25,11 +25,20 @@ 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 JWKSet JWK_SET; diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java index a4144a197f5e..aa6c8e63156e 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java @@ -29,13 +29,23 @@ 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; From 8f0260dca733647a85cac2deb6087ad3c4fd43ed Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Thu, 6 Jan 2022 15:51:20 +0100 Subject: [PATCH 06/15] HBASE-26553. Address joshelser's comments --- .../hbase/security/AccessDeniedException.java | 2 +- ...thBearerSaslClientCallbackHandlerTest.java | 1 + .../exceptions/IllegalSaslStateException.java | 6 +--- .../SaslAuthenticationException.java | 5 --- .../auth/AuthenticateCallbackHandler.java | 2 +- .../hbase/security/auth/SaslExtensions.java | 4 +-- .../security/auth/SaslExtensionsCallback.java | 2 +- ...AuthBearerExtensionsValidatorCallback.java | 26 +++++++------- .../oauthbearer/OAuthBearerTokenCallback.java | 2 +- .../OAuthBearerClientInitialResponse.java | 18 +++++++--- .../internals/OAuthBearerSaslServer.java | 10 +++--- ...arerSignedJwtValidatorCallbackHandler.java | 4 +-- ...BearerExtensionsValidatorCallbackTest.java | 36 +++++++++---------- .../OAuthBearerClientInitialResponseTest.java | 12 +++---- .../internals/OAuthBearerSaslServerTest.java | 8 ++--- ...earerSaslServerAuthenticationProvider.java | 3 +- 16 files changed, 72 insertions(+), 69 deletions(-) diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java index 259a0a4d651d..730e71af8ddc 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java @@ -33,7 +33,7 @@ public AccessDeniedException() { } public AccessDeniedException(Class clazz, String s) { - super( "AccessDenied [" + clazz.getName() + "]: " + s); + super("AccessDenied [" + clazz.getName() + "]: " + s); } public AccessDeniedException(String s) { 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 index ed229d28af48..2d5d225ef5db 100644 --- 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 @@ -89,6 +89,7 @@ public void testWithPotentiallyMultipleTokens() throws Exception { 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()); 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 index 987c32fcec49..ce7d1f7c3ddb 100644 --- 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 @@ -21,8 +21,7 @@ /** * This exception indicates unexpected requests prior to SASL authentication. - * This could be due to misconfigured security, e.g. if PLAINTEXT protocol - * is used to connect to a SASL endpoint. + * This could be due to misconfigured security. */ @InterfaceAudience.Public public class IllegalSaslStateException extends IllegalStateException { @@ -33,7 +32,4 @@ public IllegalSaslStateException(String message) { super(message); } - public IllegalSaslStateException(String message, Throwable cause) { - super(message, cause); - } } 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 index 9daa702782c1..3f4866e0f557 100644 --- 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 @@ -44,9 +44,4 @@ public class SaslAuthenticationException extends RuntimeException { public SaslAuthenticationException(String message) { super(message); } - - public SaslAuthenticationException(String message, Throwable cause) { - super(message, cause); - } - } 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 index 24db41e99ebf..1329e9ac67f7 100644 --- 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 @@ -25,7 +25,7 @@ /* * Callback handler for SASL-based authentication */ -@InterfaceAudience.Public +@InterfaceAudience.Private public interface AuthenticateCallbackHandler extends CallbackHandler { /** diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java index eb8839e37916..0f7ef6413659 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java @@ -22,7 +22,7 @@ import java.util.Map; import org.apache.yetus.audience.InterfaceAudience; -@InterfaceAudience.Public +@InterfaceAudience.Private public class SaslExtensions { /** * An "empty" instance indicating no SASL extensions @@ -38,7 +38,7 @@ public SaslExtensions(Map extensionsMap) { /** * Returns an immutable map of the extension names and their values */ - public Map map() { + public Map getExtensions() { return extensionsMap; } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java index 15f853abd175..68cf0c00e515 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java @@ -25,7 +25,7 @@ * Optional callback used for SASL mechanisms if any extensions need to be set * in the SASL exchange. */ -@InterfaceAudience.Public +@InterfaceAudience.Private public class SaslExtensionsCallback implements Callback { private SaslExtensions extensions = SaslExtensions.NO_SASL_EXTENSIONS; diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java index 05bdb62ad76a..e43340ac5c8c 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -30,14 +30,14 @@ /** * A {@code Callback} for use by the {@code SaslServer} implementation when it * needs to validate the SASL extensions for the OAUTHBEARER mechanism - * Callback handlers should use the {@link #valid(String)} + * Callback handlers should use the {@link #storeAsValid(String)} * method to communicate valid extensions back to the SASL server. * Callback handlers should use the - * {@link #error(String, String)} method to communicate validation errors back to + * {@link #storeAsError(String, String)} method to communicate validation errors back to * the SASL Server. * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown extensions must be ignored by the server. * The callback handler implementation should simply ignore unknown extensions, - * not calling {@link #error(String, String)} nor {@link #valid(String)}. + * not calling {@link #storeAsError(String, String)} nor {@link #storeAsValid(String)}. * Callback handlers should communicate other problems by raising an {@code IOException}. *

* The OAuth bearer token is provided in the callback for better context in extension validation. @@ -60,7 +60,7 @@ public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, SaslExtens /** * @return {@link OAuthBearerToken} the OAuth bearer token of the client */ - public OAuthBearerToken token() { + public OAuthBearerToken getToken() { return token; } @@ -68,7 +68,7 @@ public OAuthBearerToken token() { * @return {@link SaslExtensions} consisting of the unvalidated extension names and values that * were sent by the client */ - public SaslExtensions inputExtensions() { + public SaslExtensions getInputExtensions() { return inputExtensions; } @@ -76,7 +76,7 @@ public SaslExtensions inputExtensions() { * @return an unmodifiable {@link Map} consisting of the validated and recognized by the server * extension names and values. */ - public Map validatedExtensions() { + public Map getValidatedExtensions() { return Collections.unmodifiableMap(validatedExtensions); } @@ -84,7 +84,7 @@ public Map validatedExtensions() { * @return An immutable {@link Map} consisting of the name->error messages of extensions * which failed validation */ - public Map invalidExtensions() { + public Map getInvalidExtensions() { return Collections.unmodifiableMap(invalidExtensions); } @@ -92,21 +92,21 @@ public Map invalidExtensions() { * @return An immutable {@link Map} consisting of the extensions that have neither been * validated nor invalidated */ - public Map ignoredExtensions() { + public Map getIgnoredExtensions() { return Collections.unmodifiableMap( - subtractMap(subtractMap(inputExtensions.map(), invalidExtensions), validatedExtensions)); + subtractMap(subtractMap(inputExtensions.getExtensions(), invalidExtensions), validatedExtensions)); } /** * Validates a specific extension in the original {@code inputExtensions} map * @param extensionName - the name of the extension which was validated */ - public void valid(String extensionName) { - if (!inputExtensions.map().containsKey(extensionName)) { + public void storeAsValid(String extensionName) { + if (!inputExtensions.getExtensions().containsKey(extensionName)) { throw new IllegalArgumentException( String.format("Extension %s was not found in the original extensions", extensionName)); } - validatedExtensions.put(extensionName, inputExtensions.map().get(extensionName)); + validatedExtensions.put(extensionName, inputExtensions.getExtensions().get(extensionName)); } /** * Set the error value for a specific extension key-value pair if validation has failed @@ -116,7 +116,7 @@ public void valid(String extensionName) { * @param errorMessage * error message describing why the validation failed */ - public void error(String invalidExtensionName, String errorMessage) { + public void storeAsError(String invalidExtensionName, String errorMessage) { if (StringUtils.isEmpty(invalidExtensionName)) { throw new IllegalArgumentException("extension name must not be empty"); } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java index 6e9eeabb8652..a9e28efbc2a8 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java @@ -36,7 +36,7 @@ * update the {@code InterfaceStability} annotation and this notice once the API * is considered stable. */ -@InterfaceAudience.Public +@InterfaceAudience.Private public class OAuthBearerTokenCallback implements Callback { private OAuthBearerToken token = null; private String errorCode = null; 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 index 4b5393b2dbb7..8971c2ba3ea3 100644 --- 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 @@ -25,7 +25,10 @@ import javax.security.sasl.SaslException; import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; +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. @@ -34,6 +37,7 @@ */ @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)+"; @@ -56,11 +60,13 @@ public class OAuthBearerClientInitialResponse { public static final Pattern EXTENSION_VALUE_PATTERN = Pattern.compile(VALUE); 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"); @@ -69,21 +75,25 @@ public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { 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); SaslExtensions extensions = new SaslExtensions(properties); validateExtensions(extensions); this.saslExtensions = extensions; + LOG.trace("Sasl extensions have been validated successfully"); 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"); } /** @@ -144,7 +154,7 @@ public byte[] toBytes() { String message = String.format("n,%s,%sauth=Bearer %s%s%s%s", authzid, SEPARATOR, tokenValue, extensions, SEPARATOR, SEPARATOR); - return message.getBytes(StandardCharsets.UTF_8); + return Bytes.toBytes(message); } /** @@ -184,12 +194,12 @@ public static void validateExtensions(SaslExtensions extensions) throws SaslExce if (extensions == null) { return; } - if (extensions.map().containsKey(OAuthBearerClientInitialResponse.AUTH_KEY)) { + if (extensions.getExtensions().containsKey(OAuthBearerClientInitialResponse.AUTH_KEY)) { throw new SaslException("Extension name " + OAuthBearerClientInitialResponse.AUTH_KEY + " is invalid"); } - for (Map.Entry entry : extensions.map().entrySet()) { + for (Map.Entry entry : extensions.getExtensions().entrySet()) { String extensionName = entry.getKey(); String extensionValue = entry.getValue(); @@ -207,6 +217,6 @@ public static void validateExtensions(SaslExtensions extensions) throws SaslExce * Converts the SASLExtensions to an OAuth protocol-friendly string */ private String extensionsMessage() { - return OAuthBearerStringUtils.mkString(saslExtensions.map(), "", "", "=", SEPARATOR); + return OAuthBearerStringUtils.mkString(saslExtensions.getExtensions(), "", "", "=", SEPARATOR); } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java index 1b330a6b92de..8adb365f67c1 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java @@ -136,7 +136,7 @@ public Object getNegotiatedProperty(String propName) { if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { return tokenForNegotiatedProperty.lifetimeMs(); } - return extensions.map().get(propName); + return extensions.getExtensions().get(propName); } @Override @@ -213,16 +213,16 @@ private Map processExtensions(OAuthBearerToken token, SaslExtens } catch (IOException e) { handleCallbackError(e); } - if (!extensionsCallback.invalidExtensions().isEmpty()) { + if (!extensionsCallback.getInvalidExtensions().isEmpty()) { String errorMessage = String.format("Authentication failed: %d extensions are invalid! " - + "They are: %s", extensionsCallback.invalidExtensions().size(), - OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), + + "They are: %s", extensionsCallback.getInvalidExtensions().size(), + OAuthBearerStringUtils.mkString(extensionsCallback.getInvalidExtensions(), "", "", ": ", "; ")); LOG.debug(errorMessage); throw new SaslAuthenticationException(errorMessage); } - return extensionsCallback.validatedExtensions(); + return extensionsCallback.getValidatedExtensions(); } private static String jsonErrorResponse(String errorStatus, String errorScope, diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java index e4f8e9d4a1a4..832571fa0a57 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java @@ -109,8 +109,8 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { OAuthBearerExtensionsValidatorCallback extensionsCallback = (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.inputExtensions().map().forEach((extensionName, v) -> - extensionsCallback.valid(extensionName)); + extensionsCallback.getInputExtensions().getExtensions().forEach((extensionName, v) -> + extensionsCallback.storeAsValid(extensionName)); } else { throw new UnsupportedCallbackException(callback); } diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java index 3488a2412f3d..f5cccf825d37 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java @@ -47,12 +47,12 @@ public void testValidatedExtensionsAreReturned() { OAuthBearerExtensionsValidatorCallback callback = new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - assertTrue(callback.validatedExtensions().isEmpty()); - assertTrue(callback.invalidExtensions().isEmpty()); - callback.valid("hello"); - assertFalse(callback.validatedExtensions().isEmpty()); - assertEquals("bye", callback.validatedExtensions().get("hello")); - assertTrue(callback.invalidExtensions().isEmpty()); + assertTrue(callback.getValidatedExtensions().isEmpty()); + assertTrue(callback.getInvalidExtensions().isEmpty()); + callback.storeAsValid("hello"); + assertFalse(callback.getValidatedExtensions().isEmpty()); + assertEquals("bye", callback.getValidatedExtensions().get("hello")); + assertTrue(callback.getInvalidExtensions().isEmpty()); } @Test @@ -63,12 +63,12 @@ public void testInvalidExtensionsAndErrorMessagesAreReturned() { OAuthBearerExtensionsValidatorCallback callback = new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - assertTrue(callback.validatedExtensions().isEmpty()); - assertTrue(callback.invalidExtensions().isEmpty()); - callback.error("hello", "error"); - assertFalse(callback.invalidExtensions().isEmpty()); - assertEquals("error", callback.invalidExtensions().get("hello")); - assertTrue(callback.validatedExtensions().isEmpty()); + assertTrue(callback.getValidatedExtensions().isEmpty()); + assertTrue(callback.getInvalidExtensions().isEmpty()); + callback.storeAsError("hello", "error"); + assertFalse(callback.getInvalidExtensions().isEmpty()); + assertEquals("error", callback.getInvalidExtensions().get("hello")); + assertTrue(callback.getValidatedExtensions().isEmpty()); } /** @@ -83,12 +83,12 @@ public void testUnvalidatedExtensionsAreIgnored() { OAuthBearerExtensionsValidatorCallback callback = new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - callback.error("error", "error"); - callback.valid("valid"); + callback.storeAsError("error", "error"); + callback.storeAsValid("valid"); - assertFalse(callback.validatedExtensions().containsKey("nothing")); - assertFalse(callback.invalidExtensions().containsKey("nothing")); - assertEquals("nothing", callback.ignoredExtensions().get("nothing")); + assertFalse(callback.getValidatedExtensions().containsKey("nothing")); + assertFalse(callback.getInvalidExtensions().containsKey("nothing")); + assertEquals("nothing", callback.getIgnoredExtensions().get("nothing")); } @Test @@ -99,6 +99,6 @@ public void testCannotValidateExtensionWhichWasNotGiven() { OAuthBearerExtensionsValidatorCallback callback = new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - assertThrows(IllegalArgumentException.class, () -> callback.valid("???")); + assertThrows(IllegalArgumentException.class, () -> callback.storeAsValid("???")); } } 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 index 54e266eaeda8..927ce281c41f 100644 --- 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 @@ -101,8 +101,8 @@ public void testExtensions() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("567", response.tokenValue()); assertEquals("", response.authorizationId()); - assertEquals("valueA1, valueA2", response.extensions().map().get("propA")); - assertEquals("valueB", response.extensions().map().get("propB")); + assertEquals("valueA1, valueA2", response.extensions().getExtensions().get("propA")); + assertEquals("valueB", response.extensions().getExtensions().get("propB")); } // The example in the RFC uses `vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==` as the token @@ -115,8 +115,8 @@ public void testRfc7688Example() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); assertEquals("user@example.com", response.authorizationId()); - assertEquals("server.example.com", response.extensions().map().get("host")); - assertEquals("143", response.extensions().map().get("port")); + assertEquals("server.example.com", response.extensions().getExtensions().get("host")); + assertEquals("143", response.extensions().getExtensions().get("port")); } @Test @@ -127,14 +127,14 @@ public void testNoExtensionsFromByteArray() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); assertEquals("user@example.com", response.authorizationId()); - assertTrue(response.extensions().map().isEmpty()); + assertTrue(response.extensions().getExtensions().isEmpty()); } @Test public void testNoExtensionsFromTokenAndNullExtensions() throws Exception { OAuthBearerClientInitialResponse response = new OAuthBearerClientInitialResponse("token", null); - assertTrue(response.extensions().map().isEmpty()); + assertTrue(response.extensions().getExtensions().isEmpty()); } @Test diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java index d1a8dc5f3597..a16a6d6a969c 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java @@ -73,8 +73,8 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { OAuthBearerExtensionsValidatorCallback extensionsCallback = (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.valid("firstKey"); - extensionsCallback.valid("secondKey"); + extensionsCallback.storeAsValid("firstKey"); + extensionsCallback.storeAsValid("secondKey"); } else { throw new UnsupportedCallbackException(callback); } @@ -174,8 +174,8 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { OAuthBearerExtensionsValidatorCallback extensionsCallback = (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.error("firstKey", "is not valid"); - extensionsCallback.error("secondKey", "is not valid either"); + extensionsCallback.storeAsError("firstKey", "is not valid"); + extensionsCallback.storeAsError("secondKey", "is not valid either"); } else { throw new UnsupportedCallbackException(callback); } 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 index 60eb743a9294..b6f8078ccbe8 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -82,7 +83,7 @@ public AttemptingUserProvidingSaslServer run() throws SaslException { }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new RuntimeException("Failed to construct OAUTHBEARER SASL server"); + throw new InterruptedIOException("Failed to construct OAUTHBEARER SASL server"); } } From 8d8541730571f143f8093db2b0912ffd243aca34 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Thu, 6 Jan 2022 17:11:43 +0100 Subject: [PATCH 07/15] HBASE-26553. Move JWT related logic to hbase-server --- hbase-common/pom.xml | 4 ---- .../oauthbearer/internals/OAuthBearerSaslClient.java | 9 ++++++++- hbase-server/pom.xml | 4 ++++ .../OAuthBearerExtensionsValidatorCallback.java | 3 ++- .../oauthbearer/OAuthBearerValidatorCallback.java | 0 .../oauthbearer/internals/OAuthBearerSaslServer.java | 0 .../internals/OAuthBearerSaslServerProvider.java | 0 .../internals/knox/OAuthBearerConfigException.java | 0 .../internals/knox/OAuthBearerIllegalTokenException.java | 0 .../oauthbearer/internals/knox/OAuthBearerSignedJwt.java | 0 .../OAuthBearerSignedJwtValidatorCallbackHandler.java | 0 .../internals/knox/OAuthBearerValidationResult.java | 0 .../OAuthBearerExtensionsValidatorCallbackTest.java | 0 .../oauthbearer/OAuthBearerValidatorCallbackTest.java | 0 .../oauthbearer/internals/OAuthBearerSaslServerTest.java | 0 .../internals/knox/OAuthBearerSignedJwtTest.java | 0 ...OAuthBearerSignedJwtValidatorCallbackHandlerTest.java | 0 pom.xml | 6 +----- 18 files changed, 15 insertions(+), 11 deletions(-) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java (99%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java (100%) rename {hbase-common => hbase-server}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java (100%) rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java (100%) rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java (100%) rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java (100%) rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java (100%) rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java (100%) diff --git a/hbase-common/pom.xml b/hbase-common/pom.xml index 47541ba9d0f4..95df377a78ab 100644 --- a/hbase-common/pom.xml +++ b/hbase-common/pom.xml @@ -255,10 +255,6 @@ kerb-simplekdc test - - com.nimbusds - nimbus-jose-jwt - diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java index d76fe845da85..24f82f716a87 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -26,6 +26,7 @@ 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; @@ -180,6 +181,12 @@ private SaslExtensions retrieveCustomExtensions() throws SaslException { return extensionsCallback.extensions(); } + public static String[] mechanismNamesCompatibleWithPolicy(Map props) { + return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) + ? new String[] {} + : new String[] { OAUTHBEARER_MECHANISM}; + } + public static class OAuthBearerSaslClientFactory implements SaslClientFactory { @Override public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol, @@ -203,7 +210,7 @@ public SaslClient createSaslClient(String[] mechanisms, String authorizationId, @Override public String[] getMechanismNames(Map props) { - return OAuthBearerSaslServer.mechanismNamesCompatibleWithPolicy(props); + return OAuthBearerSaslClient.mechanismNamesCompatibleWithPolicy(props); } } } diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml index a40fb964ac79..9d45dfd932c1 100644 --- a/hbase-server/pom.xml +++ b/hbase-server/pom.xml @@ -528,6 +528,10 @@ log4j-1.2-api test + + com.nimbusds + nimbus-jose-jwt + diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java similarity index 99% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java index e43340ac5c8c..7519f5e76e17 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java @@ -94,7 +94,8 @@ public Map getInvalidExtensions() { */ public Map getIgnoredExtensions() { return Collections.unmodifiableMap( - subtractMap(subtractMap(inputExtensions.getExtensions(), invalidExtensions), validatedExtensions)); + subtractMap(subtractMap(inputExtensions.getExtensions(), invalidExtensions), + validatedExtensions)); } /** diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallback.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerProvider.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerConfigException.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerIllegalTokenException.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandler.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java rename to hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerValidationResult.java diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerValidatorCallbackTest.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServerTest.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtTest.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwtValidatorCallbackHandlerTest.java diff --git a/pom.xml b/pom.xml index b44242351f74..bd2c16271dfc 100755 --- a/pom.xml +++ b/pom.xml @@ -1821,7 +1821,7 @@ 1.9 1.5.0-4 4.0.1 - 9.15 + 9.15.2 @@ -3205,10 +3205,6 @@ net.minidev json-smart - - com.nimbusds - nimbus-jose-jwt - org.slf4j slf4j-log4j12 From dabfe95c7057b13a0e97bcb979adbb0120d21273 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Mon, 10 Jan 2022 16:52:56 +0100 Subject: [PATCH 08/15] HBASE-26655. Removed Sasl extensions --- ...earerSaslClientAuthenticationProvider.java | 16 --- .../hbase/security/auth/SaslExtensions.java | 65 --------- .../security/auth/SaslExtensionsCallback.java | 52 -------- .../OAuthBearerClientInitialResponse.java | 86 +----------- .../internals/OAuthBearerSaslClient.java | 26 +--- .../OAuthBearerClientInitialResponseTest.java | 42 +----- .../internals/OAuthBearerSaslClientTest.java | 64 +-------- ...AuthBearerExtensionsValidatorCallback.java | 126 ------------------ .../internals/OAuthBearerSaslServer.java | 38 +----- ...arerSignedJwtValidatorCallbackHandler.java | 9 -- 10 files changed, 17 insertions(+), 507 deletions(-) delete mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java delete mode 100644 hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java delete mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java 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 index b1a8d5b262ed..4976886cd5ca 100644 --- 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 @@ -38,8 +38,6 @@ 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.auth.SaslExtensions; -import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; import org.apache.hadoop.security.token.Token; @@ -89,9 +87,6 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback for (Callback callback : callbacks) { if (callback instanceof OAuthBearerTokenCallback) { handleCallback((OAuthBearerTokenCallback) callback); - } else if (callback instanceof SaslExtensionsCallback) { - handleCallback((SaslExtensionsCallback) callback, - Subject.getSubject(AccessController.getContext())); } else { throw new UnsupportedCallbackException(callback); } @@ -140,17 +135,6 @@ public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { callback.token(sortedByLifetime.last()); } } - - /** - * Attaches the first {@link SaslExtensions} found in the public credentials of the Subject - */ - private static void handleCallback(SaslExtensionsCallback extensionsCallback, Subject subject) { - if (subject != null && !subject.getPublicCredentials(SaslExtensions.class).isEmpty()) { - SaslExtensions extensions = - subject.getPublicCredentials(SaslExtensions.class).iterator().next(); - extensionsCallback.extensions(extensions); - } - } } @Override diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java deleted file mode 100644 index 0f7ef6413659..000000000000 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensions.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.hadoop.hbase.security.auth; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.apache.yetus.audience.InterfaceAudience; - -@InterfaceAudience.Private -public class SaslExtensions { - /** - * An "empty" instance indicating no SASL extensions - */ - public static final SaslExtensions NO_SASL_EXTENSIONS = - new SaslExtensions(Collections.emptyMap()); - private final Map extensionsMap; - - public SaslExtensions(Map extensionsMap) { - this.extensionsMap = Collections.unmodifiableMap(new HashMap<>(extensionsMap)); - } - - /** - * Returns an immutable map of the extension names and their values - */ - public Map getExtensions() { - return extensionsMap; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - return extensionsMap.equals(((SaslExtensions) o).extensionsMap); - } - - @Override - public String toString() { - return extensionsMap.toString(); - } - - @Override - public int hashCode() { - return extensionsMap.hashCode(); - } -} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java deleted file mode 100644 index 68cf0c00e515..000000000000 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/auth/SaslExtensionsCallback.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.hadoop.hbase.security.auth; - -import java.util.Objects; -import javax.security.auth.callback.Callback; -import org.apache.yetus.audience.InterfaceAudience; - -/** - * Optional callback used for SASL mechanisms if any extensions need to be set - * in the SASL exchange. - */ -@InterfaceAudience.Private -public class SaslExtensionsCallback implements Callback { - private SaslExtensions extensions = SaslExtensions.NO_SASL_EXTENSIONS; - - /** - * Returns always non-null {@link SaslExtensions} consisting of the extension - * names and values that are sent by the client to the server in the initial - * client SASL authentication message. The default value is - * {@link SaslExtensions#NO_SASL_EXTENSIONS} so that if this callback is - * unhandled the client will see a non-null value. - */ - public SaslExtensions extensions() { - return extensions; - } - - /** - * Sets the SASL extensions on this callback. - * - * @param extensions - * the mandatory extensions to set - */ - public void extensions(SaslExtensions extensions) { - this.extensions = Objects.requireNonNull(extensions, "extensions must not be null"); - } -} 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 index 8971c2ba3ea3..7ed7d3081dd1 100644 --- 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 @@ -23,7 +23,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.security.sasl.SaslException; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.hadoop.hbase.util.Bytes; import org.apache.yetus.audience.InterfaceAudience; @@ -54,10 +53,6 @@ public class OAuthBearerClientInitialResponse { private final String tokenValue; private final String authorizationId; - private final SaslExtensions saslExtensions; - - public static final Pattern EXTENSION_KEY_PATTERN = Pattern.compile(KEY); - public static final Pattern EXTENSION_VALUE_PATTERN = Pattern.compile(VALUE); public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { LOG.trace("Client initial response parsing started"); @@ -77,11 +72,6 @@ public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { } LOG.trace("Auth key found in client initial response"); properties.remove(AUTH_KEY); - SaslExtensions extensions = new SaslExtensions(properties); - validateExtensions(extensions); - this.saslExtensions = extensions; - LOG.trace("Sasl extensions have been validated successfully"); - Matcher authMatcher = AUTH_PATTERN.matcher(auth); if (!authMatcher.matches()) { throw new SaslException("Invalid OAUTHBEARER client first message: invalid 'auth' format"); @@ -101,16 +91,13 @@ public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { * * @param tokenValue * the mandatory token value - * @param extensions - * the optional extensions * @throws SaslException * if any extension name or value fails to conform to the required * regular expression as defined by the specification, or if the * reserved {@code auth} appears as a key */ - public OAuthBearerClientInitialResponse(String tokenValue, SaslExtensions extensions) - throws SaslException { - this(tokenValue, "", extensions); + public OAuthBearerClientInitialResponse(String tokenValue) { + this(tokenValue, ""); } /** @@ -120,39 +107,21 @@ public OAuthBearerClientInitialResponse(String tokenValue, SaslExtensions extens * the mandatory token value * @param authorizationId * the optional authorization ID - * @param extensions - * the optional extensions * @throws SaslException * if any extension name or value fails to conform to the required * regular expression as defined by the specification, or if the * reserved {@code auth} appears as a key */ - public OAuthBearerClientInitialResponse(String tokenValue, String authorizationId, - SaslExtensions extensions) throws SaslException { + public OAuthBearerClientInitialResponse(String tokenValue, String authorizationId) { this.tokenValue = Objects.requireNonNull(tokenValue, "token value must not be null"); this.authorizationId = authorizationId == null ? "" : authorizationId; - validateExtensions(extensions); - this.saslExtensions = extensions != null ? extensions : SaslExtensions.NO_SASL_EXTENSIONS; - } - - /** - * Return the always non-null extensions - * - * @return the always non-null extensions - */ - public SaslExtensions extensions() { - return saslExtensions; } public byte[] toBytes() { String authzid = authorizationId.isEmpty() ? "" : "a=" + authorizationId; - String extensions = extensionsMessage(); - if (extensions.length() > 0) { - extensions = SEPARATOR + extensions; - } - String message = String.format("n,%s,%sauth=Bearer %s%s%s%s", authzid, - SEPARATOR, tokenValue, extensions, SEPARATOR, SEPARATOR); + String message = String.format("n,%s,%sauth=Bearer %s%s%s", authzid, + SEPARATOR, tokenValue, SEPARATOR, SEPARATOR); return Bytes.toBytes(message); } @@ -174,49 +143,4 @@ public String tokenValue() { public String authorizationId() { return authorizationId; } - - /** - * Validates that the given extensions conform to the standard. - * They should also not contain the reserve key name - * {@link OAuthBearerClientInitialResponse#AUTH_KEY} - * - * @param extensions - * optional extensions to validate - * @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 - * - * @see RFC 7628, - * Section 3.1 - */ - public static void validateExtensions(SaslExtensions extensions) throws SaslException { - if (extensions == null) { - return; - } - if (extensions.getExtensions().containsKey(OAuthBearerClientInitialResponse.AUTH_KEY)) { - throw new SaslException("Extension name " + - OAuthBearerClientInitialResponse.AUTH_KEY + " is invalid"); - } - - for (Map.Entry entry : extensions.getExtensions().entrySet()) { - String extensionName = entry.getKey(); - String extensionValue = entry.getValue(); - - if (!EXTENSION_KEY_PATTERN.matcher(extensionName).matches()) { - throw new SaslException("Extension name " + extensionName + " is invalid"); - } - if (!EXTENSION_VALUE_PATTERN.matcher(extensionValue).matches()) { - throw new SaslException("Extension value (" + extensionValue + ") for extension " + - extensionName + " is invalid"); - } - } - } - - /** - * Converts the SASLExtensions to an OAuth protocol-friendly string - */ - private String extensionsMessage() { - return OAuthBearerStringUtils.mkString(saslExtensions.getExtensions(), "", "", "=", SEPARATOR); - } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java index 24f82f716a87..fc71f1a56427 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java @@ -32,8 +32,6 @@ import javax.security.sasl.SaslException; import org.apache.hadoop.hbase.exceptions.IllegalSaslStateException; import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; -import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; import org.apache.yetus.audience.InterfaceAudience; @@ -45,9 +43,7 @@ * 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}. Said handler can also optionally handle an instance of - * {@link SaslExtensionsCallback} to return any extensions generated by the - * {@code login()} event on the {@code LoginContext}. + * {@code LoginContext}. * * @see RFC 6750 Section 2.1 * @@ -95,12 +91,8 @@ public byte[] evaluateChallenge(byte[] challenge) throws SaslException { throw new SaslException("Expected empty challenge"); } callbackHandler().handle(new Callback[] {callback}); - SaslExtensions extensions = retrieveCustomExtensions(); - setState(State.RECEIVE_SERVER_FIRST_MESSAGE); - - return new OAuthBearerClientInitialResponse(callback.token().value(), extensions) - .toBytes(); + 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); @@ -167,20 +159,6 @@ private void setState(State state) { this.state = state; } - private SaslExtensions retrieveCustomExtensions() throws SaslException { - SaslExtensionsCallback extensionsCallback = new SaslExtensionsCallback(); - try { - callbackHandler().handle(new Callback[] {extensionsCallback}); - } catch (UnsupportedCallbackException e) { - LOG.debug("Extensions callback is not supported by client callback handler {}, " - + "no extensions will be added", callbackHandler()); - } catch (Exception e) { - throw new SaslException("SASL extensions could not be obtained", e); - } - - return extensionsCallback.extensions(); - } - public static String[] mechanismNamesCompatibleWithPolicy(Map props) { return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) ? new String[] {} 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 index 927ce281c41f..86e7d46ea697 100644 --- 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 @@ -18,14 +18,8 @@ package org.apache.hadoop.hbase.security.oauthbearer.internals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; -import javax.security.sasl.SaslException; import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.junit.ClassRule; @@ -42,13 +36,11 @@ public class OAuthBearerClientInitialResponseTest { Test how a client would build a response */ @Test - public void testBuildClientResponseToBytes() throws Exception { - String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001nineteen=42\u0001\u0001"; + public void testBuildClientResponseToBytes() { + String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; - Map extensions = new HashMap<>(); - extensions.put("nineteen", "42"); OAuthBearerClientInitialResponse response = - new OAuthBearerClientInitialResponse("123.345.567", new SaslExtensions(extensions)); + new OAuthBearerClientInitialResponse("123.345.567"); String message = new String(response.toBytes(), StandardCharsets.UTF_8); @@ -57,7 +49,7 @@ public void testBuildClientResponseToBytes() throws Exception { @Test public void testBuildServerResponseToBytes() throws Exception { - String serverMessage = "n,,\u0001auth=Bearer 123.345.567\u0001nineteen=42\u0001\u0001"; + String serverMessage = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; OAuthBearerClientInitialResponse response = new OAuthBearerClientInitialResponse(serverMessage.getBytes(StandardCharsets.UTF_8)); @@ -66,15 +58,6 @@ public void testBuildServerResponseToBytes() throws Exception { assertEquals(serverMessage, message); } - @Test - public void testThrowsSaslExceptionOnInvalidExtensionKey() throws Exception { - Map extensions = new HashMap<>(); - extensions.put("19", "42"); // keys can only be a-z - assertThrows( - SaslException.class, () -> new OAuthBearerClientInitialResponse("123.345.567", - new SaslExtensions(extensions))); - } - @Test public void testToken() throws Exception { String message = "n,,\u0001auth=Bearer 123.345.567\u0001\u0001"; @@ -101,8 +84,6 @@ public void testExtensions() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("567", response.tokenValue()); assertEquals("", response.authorizationId()); - assertEquals("valueA1, valueA2", response.extensions().getExtensions().get("propA")); - assertEquals("valueB", response.extensions().getExtensions().get("propB")); } // The example in the RFC uses `vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==` as the token @@ -115,8 +96,6 @@ public void testRfc7688Example() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); assertEquals("user@example.com", response.authorizationId()); - assertEquals("server.example.com", response.extensions().getExtensions().get("host")); - assertEquals("143", response.extensions().getExtensions().get("port")); } @Test @@ -127,18 +106,5 @@ public void testNoExtensionsFromByteArray() throws Exception { new OAuthBearerClientInitialResponse(message.getBytes(StandardCharsets.UTF_8)); assertEquals("vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg", response.tokenValue()); assertEquals("user@example.com", response.authorizationId()); - assertTrue(response.extensions().getExtensions().isEmpty()); - } - - @Test - public void testNoExtensionsFromTokenAndNullExtensions() throws Exception { - OAuthBearerClientInitialResponse response = - new OAuthBearerClientInitialResponse("token", null); - assertTrue(response.extensions().getExtensions().isEmpty()); - } - - @Test - public void testValidateNullExtensions() throws Exception { - OAuthBearerClientInitialResponse.validateExtensions(null); } } diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java index 7bcf55cbe167..0267084d35e7 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java @@ -18,17 +18,11 @@ package org.apache.hadoop.hbase.security.oauthbearer.internals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; import javax.security.auth.callback.Callback; import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.sasl.SaslException; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; -import org.apache.hadoop.hbase.security.auth.SaslExtensionsCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; import org.apache.hadoop.hbase.testclassification.MiscTests; @@ -43,27 +37,7 @@ public class OAuthBearerSaslClientTest { public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(OAuthBearerSaslClientTest.class); - private static final Map TEST_PROPERTIES = new LinkedHashMap() { - { - put("One", "1"); - put("Two", "2"); - put("Three", "3"); - } - }; - private SaslExtensions testExtensions = new SaslExtensions(TEST_PROPERTIES); - private final String errorMessage = "Error as expected!"; - - public class ExtensionsCallbackHandler implements AuthenticateCallbackHandler { - private boolean configured = false; - private boolean toThrow; - - ExtensionsCallbackHandler(boolean toThrow) { - this.toThrow = toThrow; - } - - public boolean configured() { - return configured; - } + public static class ExtensionsCallbackHandler implements AuthenticateCallbackHandler { @Override public void handle(Callback[] callbacks) throws UnsupportedCallbackException { @@ -82,12 +56,6 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { return "principalName"; } }); - } else if (callback instanceof SaslExtensionsCallback) { - if (toThrow) { - throw new RuntimeException(errorMessage); - } else { - ((SaslExtensionsCallback) callback).extensions(testExtensions); - } } else { throw new UnsupportedCallbackException(callback); } @@ -98,37 +66,11 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { @Test public void testAttachesExtensionsToFirstClientMessage() throws Exception { String expectedToken = new String( - new OAuthBearerClientInitialResponse("", testExtensions).toBytes(), StandardCharsets.UTF_8); - OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); + 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); } - @Test - public void testNoExtensionsDoesNotAttachAnythingToFirstClientMessage() throws Exception { - TEST_PROPERTIES.clear(); - testExtensions = new SaslExtensions(TEST_PROPERTIES); - String expectedToken = new String(new OAuthBearerClientInitialResponse("", - new SaslExtensions(TEST_PROPERTIES)).toBytes(), StandardCharsets.UTF_8); - OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(false)); - - String message = new String(client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8); - - assertEquals(expectedToken, message); - } - - @Test - public void testWrapsExtensionsCallbackHandlingErrorInSaslExceptionInFirstClientMessage() { - OAuthBearerSaslClient client = new OAuthBearerSaslClient(new ExtensionsCallbackHandler(true)); - try { - client.evaluateChallenge("".getBytes(StandardCharsets.UTF_8)); - fail("Should have failed with " + SaslException.class.getName()); - } catch (SaslException e) { - // assert it has caught our expected exception - assertEquals(RuntimeException.class, e.getCause().getClass()); - assertEquals(errorMessage, e.getCause().getMessage()); - } - } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java deleted file mode 100644 index 7519f5e76e17..000000000000 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallback.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.hadoop.hbase.security.oauthbearer; - -import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils.subtractMap; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import javax.security.auth.callback.Callback; -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; -import org.apache.yetus.audience.InterfaceAudience; - -/** - * A {@code Callback} for use by the {@code SaslServer} implementation when it - * needs to validate the SASL extensions for the OAUTHBEARER mechanism - * Callback handlers should use the {@link #storeAsValid(String)} - * method to communicate valid extensions back to the SASL server. - * Callback handlers should use the - * {@link #storeAsError(String, String)} method to communicate validation errors back to - * the SASL Server. - * As per RFC-7628 (https://tools.ietf.org/html/rfc7628#section-3.1), unknown extensions must be ignored by the server. - * The callback handler implementation should simply ignore unknown extensions, - * not calling {@link #storeAsError(String, String)} nor {@link #storeAsValid(String)}. - * Callback handlers should communicate other problems by raising an {@code IOException}. - *

- * The OAuth bearer token is provided in the callback for better context in extension validation. - * It is very important that token validation is done in its own - * {@link OAuthBearerValidatorCallback} irregardless of provided extensions, as they are inherently - * insecure. - */ -@InterfaceAudience.Public -public class OAuthBearerExtensionsValidatorCallback implements Callback { - private final OAuthBearerToken token; - private final SaslExtensions inputExtensions; - private final Map validatedExtensions = new HashMap<>(); - private final Map invalidExtensions = new HashMap<>(); - - public OAuthBearerExtensionsValidatorCallback(OAuthBearerToken token, SaslExtensions extensions) { - this.token = Objects.requireNonNull(token); - this.inputExtensions = Objects.requireNonNull(extensions); - } - - /** - * @return {@link OAuthBearerToken} the OAuth bearer token of the client - */ - public OAuthBearerToken getToken() { - return token; - } - - /** - * @return {@link SaslExtensions} consisting of the unvalidated extension names and values that - * were sent by the client - */ - public SaslExtensions getInputExtensions() { - return inputExtensions; - } - - /** - * @return an unmodifiable {@link Map} consisting of the validated and recognized by the server - * extension names and values. - */ - public Map getValidatedExtensions() { - return Collections.unmodifiableMap(validatedExtensions); - } - - /** - * @return An immutable {@link Map} consisting of the name->error messages of extensions - * which failed validation - */ - public Map getInvalidExtensions() { - return Collections.unmodifiableMap(invalidExtensions); - } - - /** - * @return An immutable {@link Map} consisting of the extensions that have neither been - * validated nor invalidated - */ - public Map getIgnoredExtensions() { - return Collections.unmodifiableMap( - subtractMap(subtractMap(inputExtensions.getExtensions(), invalidExtensions), - validatedExtensions)); - } - - /** - * Validates a specific extension in the original {@code inputExtensions} map - * @param extensionName - the name of the extension which was validated - */ - public void storeAsValid(String extensionName) { - if (!inputExtensions.getExtensions().containsKey(extensionName)) { - throw new IllegalArgumentException( - String.format("Extension %s was not found in the original extensions", extensionName)); - } - validatedExtensions.put(extensionName, inputExtensions.getExtensions().get(extensionName)); - } - /** - * Set the error value for a specific extension key-value pair if validation has failed - * - * @param invalidExtensionName - * the mandatory extension name which caused the validation failure - * @param errorMessage - * error message describing why the validation failed - */ - public void storeAsError(String invalidExtensionName, String errorMessage) { - if (StringUtils.isEmpty(invalidExtensionName)) { - throw new IllegalArgumentException("extension name must not be empty"); - } - this.invalidExtensions.put(invalidExtensionName, errorMessage); - } -} 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 index 8adb365f67c1..ab72e1958862 100644 --- 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 @@ -33,9 +33,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; import org.apache.yetus.audience.InterfaceAudience; @@ -63,7 +60,6 @@ public class OAuthBearerSaslServer implements SaslServer { private boolean complete; private OAuthBearerToken tokenForNegotiatedProperty = null; private String errorMessage = null; - private SaslExtensions extensions; public OAuthBearerSaslServer(CallbackHandler callbackHandler) { if (!(callbackHandler instanceof AuthenticateCallbackHandler)) { @@ -101,8 +97,7 @@ public byte[] evaluateResponse(byte[] response) OAuthBearerClientInitialResponse clientResponse; clientResponse = new OAuthBearerClientInitialResponse(response); - return process(clientResponse.tokenValue(), clientResponse.authorizationId(), - clientResponse.extensions()); + return process(clientResponse.tokenValue(), clientResponse.authorizationId()); } catch (SaslAuthenticationException e) { LOG.error("SASL authentication error", e); throw e; @@ -136,7 +131,7 @@ public Object getNegotiatedProperty(String propName) { if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { return tokenForNegotiatedProperty.lifetimeMs(); } - return extensions.getExtensions().get(propName); + throw new IllegalArgumentException("Unknown propName: " + propName); } @Override @@ -164,10 +159,9 @@ public byte[] wrap(byte[] outgoing, int offset, int len) { public void dispose() { complete = false; tokenForNegotiatedProperty = null; - extensions = null; } - private byte[] process(String tokenValue, String authorizationId, SaslExtensions extensions) + private byte[] process(String tokenValue, String authorizationId) throws SaslException { OAuthBearerValidatorCallback callback = new OAuthBearerValidatorCallback(tokenValue); try { @@ -193,38 +187,12 @@ private byte[] process(String tokenValue, String authorizationId, SaslExtensions authorizationId, token.principalName())); } - Map validExtensions = processExtensions(token, extensions); - tokenForNegotiatedProperty = token; - this.extensions = new SaslExtensions(validExtensions); complete = true; LOG.debug("Successfully authenticate User={}", token.principalName()); return new byte[0]; } - private Map processExtensions(OAuthBearerToken token, SaslExtensions extensions) - throws SaslException { - OAuthBearerExtensionsValidatorCallback - extensionsCallback = new OAuthBearerExtensionsValidatorCallback(token, extensions); - try { - callbackHandler.handle(new Callback[] {extensionsCallback}); - } catch (UnsupportedCallbackException e) { - // backwards compatibility - no extensions will be added - } catch (IOException e) { - handleCallbackError(e); - } - if (!extensionsCallback.getInvalidExtensions().isEmpty()) { - String errorMessage = String.format("Authentication failed: %d extensions are invalid! " - + "They are: %s", extensionsCallback.getInvalidExtensions().size(), - OAuthBearerStringUtils.mkString(extensionsCallback.getInvalidExtensions(), - "", "", ": ", "; ")); - LOG.debug(errorMessage); - throw new SaslAuthenticationException(errorMessage); - } - - return extensionsCallback.getValidatedExtensions(); - } - private static String jsonErrorResponse(String errorStatus, String errorScope, String errorOpenIDConfiguration) { JSONObject jsonObject = new JSONObject(); 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 index 832571fa0a57..6a4418f75597 100644 --- 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 @@ -30,7 +30,6 @@ 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.OAuthBearerExtensionsValidatorCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; @@ -67,9 +66,6 @@ * clock skew (the default is 0) * * - * It also recognizes {@link OAuthBearerExtensionsValidatorCallback} and validates - * every extension passed to it. - * * This class is based on Kafka's OAuthBearerUnsecuredValidatorCallbackHandler. */ @InterfaceAudience.Public @@ -106,11 +102,6 @@ public void handle(Callback[] callbacks) throws UnsupportedCallbackException { validationCallback.error(failureScope != null ? "insufficient_scope" : "invalid_token", failureScope, failureReason.failureOpenIdConfig()); } - } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { - OAuthBearerExtensionsValidatorCallback extensionsCallback = - (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.getInputExtensions().getExtensions().forEach((extensionName, v) -> - extensionsCallback.storeAsValid(extensionName)); } else { throw new UnsupportedCallbackException(callback); } From f34ca998d0f20dcc81770e11e742e65427c0d2fe Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Mon, 10 Jan 2022 17:52:13 +0100 Subject: [PATCH 09/15] HBASE-26655. Remove more extension code, move client logic out of common project --- .../oauthbearer/OAuthBearerTokenCallback.java | 0 .../internals/OAuthBearerSaslClient.java | 0 .../OAuthBearerSaslClientProvider.java | 0 .../security/token/OAuthBearerTokenUtil.java | 0 .../OAuthBearerTokenCallbackTest.java | 0 .../internals/OAuthBearerSaslClientTest.java | 0 ...BearerExtensionsValidatorCallbackTest.java | 104 --------------- .../oauthbearer/OAuthBearerTokenMock.java | 0 .../internals/OAuthBearerSaslServerTest.java | 121 +----------------- 9 files changed, 6 insertions(+), 219 deletions(-) rename {hbase-common => hbase-client}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java (100%) rename {hbase-common => hbase-client}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java (100%) rename {hbase-common => hbase-client}/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java (100%) rename {hbase-common => hbase-client}/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java (100%) rename {hbase-common => hbase-client}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java (100%) rename {hbase-common => hbase-client}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java (100%) delete mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java rename {hbase-common => hbase-server}/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java (100%) diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java rename to hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallback.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java rename to hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClient.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java rename to hbase-client/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientProvider.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java rename to hbase-client/src/main/java/org/apache/hadoop/hbase/security/token/OAuthBearerTokenUtil.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java rename to hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenCallbackTest.java diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java rename to hbase-client/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslClientTest.java diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java deleted file mode 100644 index f5cccf825d37..000000000000 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerExtensionsValidatorCallbackTest.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import java.util.HashMap; -import java.util.Map; -import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; -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 OAuthBearerExtensionsValidatorCallbackTest { - @ClassRule - public static final HBaseClassTestRule CLASS_RULE = - HBaseClassTestRule.forClass(OAuthBearerExtensionsValidatorCallbackTest.class); - - private static final OAuthBearerToken TOKEN = new OAuthBearerTokenMock(); - - @Test - public void testValidatedExtensionsAreReturned() { - Map extensions = new HashMap<>(); - extensions.put("hello", "bye"); - - OAuthBearerExtensionsValidatorCallback callback = - new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - - assertTrue(callback.getValidatedExtensions().isEmpty()); - assertTrue(callback.getInvalidExtensions().isEmpty()); - callback.storeAsValid("hello"); - assertFalse(callback.getValidatedExtensions().isEmpty()); - assertEquals("bye", callback.getValidatedExtensions().get("hello")); - assertTrue(callback.getInvalidExtensions().isEmpty()); - } - - @Test - public void testInvalidExtensionsAndErrorMessagesAreReturned() { - Map extensions = new HashMap<>(); - extensions.put("hello", "bye"); - - OAuthBearerExtensionsValidatorCallback callback = - new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - - assertTrue(callback.getValidatedExtensions().isEmpty()); - assertTrue(callback.getInvalidExtensions().isEmpty()); - callback.storeAsError("hello", "error"); - assertFalse(callback.getInvalidExtensions().isEmpty()); - assertEquals("error", callback.getInvalidExtensions().get("hello")); - assertTrue(callback.getValidatedExtensions().isEmpty()); - } - - /** - * Extensions that are neither validated or invalidated must not be present in either maps - */ - @Test - public void testUnvalidatedExtensionsAreIgnored() { - Map extensions = new HashMap<>(); - extensions.put("valid", "valid"); - extensions.put("error", "error"); - extensions.put("nothing", "nothing"); - - OAuthBearerExtensionsValidatorCallback callback = - new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - callback.storeAsError("error", "error"); - callback.storeAsValid("valid"); - - assertFalse(callback.getValidatedExtensions().containsKey("nothing")); - assertFalse(callback.getInvalidExtensions().containsKey("nothing")); - assertEquals("nothing", callback.getIgnoredExtensions().get("nothing")); - } - - @Test - public void testCannotValidateExtensionWhichWasNotGiven() { - Map extensions = new HashMap<>(); - extensions.put("hello", "bye"); - - OAuthBearerExtensionsValidatorCallback callback = - new OAuthBearerExtensionsValidatorCallback(TOKEN, new SaslExtensions(extensions)); - - assertThrows(IllegalArgumentException.class, () -> callback.storeAsValid("???")); - } -} diff --git a/hbase-common/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 similarity index 100% rename from hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java rename to hbase-server/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerTokenMock.java 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 index a16a6d6a969c..377371e7dbcd 100644 --- 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 @@ -20,29 +20,17 @@ 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.assertNull; 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.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.UnsupportedCallbackException; 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.auth.AuthenticateCallbackHandler; -import org.apache.hadoop.hbase.security.auth.SaslExtensions; import org.apache.hadoop.hbase.security.oauthbearer.JwtTestUtils; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenMock; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; 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; @@ -59,28 +47,8 @@ public class OAuthBearerSaslServerTest { HBaseClassTestRule.forClass(OAuthBearerSaslServerTest.class); private static final Configuration CONFIGS; - private static final AuthenticateCallbackHandler EXTENSIONS_VALIDATOR_CALLBACK_HANDLER; static { CONFIGS = new Configuration(); - EXTENSIONS_VALIDATOR_CALLBACK_HANDLER = new OAuthBearerSignedJwtValidatorCallbackHandler() { - @Override - public void handle(Callback[] callbacks) throws UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof OAuthBearerValidatorCallback) { - OAuthBearerValidatorCallback validationCallback = - (OAuthBearerValidatorCallback) callback; - validationCallback.token(new OAuthBearerTokenMock()); - } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { - OAuthBearerExtensionsValidatorCallback extensionsCallback = - (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.storeAsValid("firstKey"); - extensionsCallback.storeAsValid("secondKey"); - } else { - throw new UnsupportedCallbackException(callback); - } - } - } - }; } private String JWT; @@ -117,80 +85,6 @@ public void negotiatedProperty() throws Exception { OAuthBearerSaslServer.CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY)); } - /** - * SASL Extensions that are validated by the callback handler should be accessible through - * the {@code #getNegotiatedProperty()} method - */ - @Test - public void savesCustomExtensionAsNegotiatedProperty() throws Exception { - Map customExtensions = new HashMap<>(); - customExtensions.put("firstKey", "value1"); - customExtensions.put("secondKey", "value2"); - - byte[] nextChallenge = saslServer - .evaluateResponse(clientInitialResponse(null, false, customExtensions)); - - assertTrue("Next challenge is not empty", nextChallenge.length == 0); - assertEquals("value1", saslServer.getNegotiatedProperty("firstKey")); - assertEquals("value2", saslServer.getNegotiatedProperty("secondKey")); - } - - /** - * SASL Extensions that were not recognized (neither validated nor invalidated) - * by the callback handler must not be accessible through the {@code #getNegotiatedProperty()} - * method - */ - @Test - public void unrecognizedExtensionsAreNotSaved() throws Exception { - saslServer = new OAuthBearerSaslServer(EXTENSIONS_VALIDATOR_CALLBACK_HANDLER); - Map customExtensions = new HashMap<>(); - customExtensions.put("firstKey", "value1"); - customExtensions.put("secondKey", "value1"); - customExtensions.put("thirdKey", "value1"); - - byte[] nextChallenge = saslServer - .evaluateResponse(clientInitialResponse(null, false, customExtensions)); - - assertTrue("Next challenge is not empty", nextChallenge.length == 0); - assertNull("Extensions not recognized by the server must be ignored", - saslServer.getNegotiatedProperty("thirdKey")); - } - - /** - * If the callback handler handles the `OAuthBearerExtensionsValidatorCallback` - * and finds an invalid extension, SaslServer should throw an authentication exception - */ - @Test - public void throwsAuthenticationExceptionOnInvalidExtensions() { - OAuthBearerSignedJwtValidatorCallbackHandler invalidHandler = - new OAuthBearerSignedJwtValidatorCallbackHandler() { - @Override - public void handle(Callback[] callbacks) throws UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof OAuthBearerValidatorCallback) { - OAuthBearerValidatorCallback validationCallback = - (OAuthBearerValidatorCallback) callback; - validationCallback.token(new OAuthBearerTokenMock()); - } else if (callback instanceof OAuthBearerExtensionsValidatorCallback) { - OAuthBearerExtensionsValidatorCallback extensionsCallback = - (OAuthBearerExtensionsValidatorCallback) callback; - extensionsCallback.storeAsError("firstKey", "is not valid"); - extensionsCallback.storeAsError("secondKey", "is not valid either"); - } else { - throw new UnsupportedCallbackException(callback); - } - } - } - }; - saslServer = new OAuthBearerSaslServer(invalidHandler); - Map customExtensions = new HashMap<>(); - customExtensions.put("firstKey", "value"); - customExtensions.put("secondKey", "value"); - - assertThrows(SaslAuthenticationException.class, - () -> saslServer.evaluateResponse(clientInitialResponse(null, false, customExtensions))); - } - @Test public void authorizatonIdEqualsAuthenticationId() throws Exception { byte[] nextChallenge = saslServer @@ -206,23 +100,20 @@ public void authorizatonIdNotEqualsAuthenticationId() { @Test public void illegalToken() throws Exception { - byte[] bytes = saslServer.evaluateResponse(clientInitialResponse(null, true, - Collections.emptyMap())); + 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, IOException { - return clientInitialResponse(authorizationId, false, Collections.emptyMap()); + throws OAuthBearerConfigException { + return clientInitialResponse(authorizationId, false); } - private byte[] clientInitialResponse(String authorizationId, boolean illegalToken, - Map customExtensions) - throws OAuthBearerConfigException, IOException { + private byte[] clientInitialResponse(String authorizationId, boolean illegalToken) + throws OAuthBearerConfigException { String compactSerialization = JWT; String tokenValue = compactSerialization + (illegalToken ? "AB" : ""); - return new OAuthBearerClientInitialResponse(tokenValue, authorizationId, - new SaslExtensions(customExtensions)).toBytes(); + return new OAuthBearerClientInitialResponse(tokenValue, authorizationId).toBytes(); } } From c8a056e54a41fdc7dc22d33cb90a2a90813864d2 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Tue, 11 Jan 2022 21:29:53 +0100 Subject: [PATCH 10/15] HBASE-26655. Address a few review comments --- .../hadoop/hbase/security/AccessDeniedException.java | 2 +- .../oauthbearer/internals/OAuthBearerSaslClient.java | 3 +++ .../OAuthBearerSaslClientAuthenticationProvider.java | 9 +++++++-- .../hbase/security/token/OAuthBearerTokenUtil.java | 6 +++--- .../oauthbearer/internals/OAuthBearerSaslServer.java | 5 ++++- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java index 730e71af8ddc..259a0a4d651d 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/AccessDeniedException.java @@ -33,7 +33,7 @@ public AccessDeniedException() { } public AccessDeniedException(Class clazz, String s) { - super("AccessDenied [" + clazz.getName() + "]: " + s); + super( "AccessDenied [" + clazz.getName() + "]: " + s); } public AccessDeniedException(String s) { 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 index fc71f1a56427..cc937774c26f 100644 --- 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 @@ -147,6 +147,9 @@ public Object getNegotiatedProperty(String propName) { if (!isComplete()) { throw new IllegalStateException("Authentication exchange has not completed"); } + if (Sasl.QOP.equals(propName)) { + return "auth"; + } return null; } 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 index 4976886cd5ca..5c27fa2776a4 100644 --- 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 @@ -101,12 +101,17 @@ private void handleCallback(OAuthBearerTokenCallback callback) throws IOExceptio 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"); - callback.token(privateCredentials.iterator().next()); + return privateCredentials.iterator().next(); } else { /* * There a very small window of time upon token refresh (on the order of milliseconds) @@ -132,7 +137,7 @@ public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { sortedByLifetime.size(), new Date(sortedByLifetime.first().lifetimeMs()), new Date(sortedByLifetime.last().lifetimeMs())); } - callback.token(sortedByLifetime.last()); + return sortedByLifetime.last(); } } } 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 index e4e8e0b3441e..da8687399948 100644 --- 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 @@ -50,7 +50,7 @@ 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) { + 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() { @@ -61,11 +61,11 @@ public static void addTokenForUser(User user, String encodedToken) { } @Override public long lifetimeMs() { - return 0; + return lifetimeMs; } @Override public String principalName() { - return null; + return user.getName(); } }; subject.getPrivateCredentials().add(jwt); 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 index ab72e1958862..475f69d6f346 100644 --- 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 @@ -131,7 +131,10 @@ public Object getNegotiatedProperty(String propName) { if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { return tokenForNegotiatedProperty.lifetimeMs(); } - throw new IllegalArgumentException("Unknown propName: " + propName); + if (Sasl.QOP.equals(propName)) { + return "auth"; + } + return null; } @Override From 32b95cf9435d91d9f01bf1a5a7bc82f82f248874 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Wed, 12 Jan 2022 00:05:06 +0100 Subject: [PATCH 11/15] HBASE-26655. Build fix --- .../hadoop/hbase/jwt/client/example/JwtClientExample.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 78817e0347cb..65eafc8edad6 100644 --- 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 @@ -71,7 +71,7 @@ public JwtClientExample() { UserProvider provider = UserProvider.instantiate(conf); User user = provider.getCurrent(); - OAuthBearerTokenUtil.addTokenForUser(user, JWT_TOKEN); + OAuthBearerTokenUtil.addTokenForUser(user, JWT_TOKEN, 0); LOG.info("JWT token added"); try (final Connection conn = ConnectionFactory.createConnection(conf, user)) { From 492068e4ecef92d941d04bd6b6f362e51d30086c Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Wed, 12 Jan 2022 09:01:38 +0100 Subject: [PATCH 12/15] HBASE-26655. Unit test fix --- .../internals/knox/OAuthBearerSignedJwtTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index f4e9bfac7692..4b72f88bd6c4 100644 --- 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 @@ -40,6 +40,7 @@ public class OAuthBearerSignedJwtTest { 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; @@ -55,7 +56,7 @@ public void validCompactSerialization() throws JOSEException { String subject = "foo"; LocalDate issuedAt = LocalDate.now(ZONE_ID); - LocalDate expirationTime = issuedAt.plusDays(1); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); OAuthBearerSignedJwt jws = new OAuthBearerSignedJwt(validCompactSerialization, JWK_SET) @@ -74,7 +75,7 @@ public void validCompactSerialization() throws JOSEException { public void missingPrincipal() throws JOSEException { String subject = null; LocalDate issuedAt = LocalDate.now(ZONE_ID); - LocalDate expirationTime = issuedAt.plusDays(1); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); assertThrows(OAuthBearerIllegalTokenException.class, @@ -85,7 +86,7 @@ public void missingPrincipal() throws JOSEException { public void blankPrincipalName() throws JOSEException { String subject = " "; LocalDate issuedAt = LocalDate.now(ZONE_ID); - LocalDate expirationTime = issuedAt.plusDays(1); + LocalDate expirationTime = issuedAt.plusDays(EXP_DAYS); String validCompactSerialization = compactSerialization(subject, issuedAt, expirationTime); assertThrows(OAuthBearerIllegalTokenException.class, From 2cbebfa7ad1e29588ee80aaaf792ff8e21981b03 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Thu, 13 Jan 2022 20:58:57 +0100 Subject: [PATCH 13/15] HBASE-26655. Set QoP for authentication only (no encryption yet) --- .../security/oauthbearer/internals/OAuthBearerSaslClient.java | 3 ++- .../security/oauthbearer/internals/OAuthBearerSaslServer.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 index cc937774c26f..ca3a1aeaf1c1 100644 --- 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 @@ -31,6 +31,7 @@ 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; @@ -148,7 +149,7 @@ public Object getNegotiatedProperty(String propName) { throw new IllegalStateException("Authentication exchange has not completed"); } if (Sasl.QOP.equals(propName)) { - return "auth"; + return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop(); } return 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 index 475f69d6f346..2b701fd3b735 100644 --- 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 @@ -32,6 +32,7 @@ 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.OAuthBearerValidatorCallback; @@ -132,7 +133,7 @@ public Object getNegotiatedProperty(String propName) { return tokenForNegotiatedProperty.lifetimeMs(); } if (Sasl.QOP.equals(propName)) { - return "auth"; + return SaslUtil.QualityOfProtection.AUTHENTICATION.getSaslQop(); } return null; } From 94abe99570ac6071b151bee7c3c5309479b9b669 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Wed, 19 Jan 2022 13:54:44 +0100 Subject: [PATCH 14/15] HBASE-26655. Throw exception if configuration is inconsistent --- .../internals/OAuthBearerSaslClient.java | 11 ++--- .../OAuthBearerSaslClientProvider.java | 2 +- ...earerSaslClientAuthenticationProvider.java | 2 +- .../security/token/OAuthBearerTokenUtil.java | 1 - ...thBearerSaslClientCallbackHandlerTest.java | 5 +-- ...StringUtils.java => OAuthBearerUtils.java} | 42 ++++++------------- .../OAuthBearerClientInitialResponse.java | 4 +- .../internals/OAuthBearerSaslServer.java | 11 ++--- .../OAuthBearerSaslServerProvider.java | 2 +- ...arerSignedJwtValidatorCallbackHandler.java | 2 +- 10 files changed, 27 insertions(+), 55 deletions(-) rename hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/{OAuthBearerStringUtils.java => OAuthBearerUtils.java} (56%) 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 index ca3a1aeaf1c1..fb5e8f3ff338 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer.internals; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -33,6 +33,7 @@ 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.OAuthBearerUtils; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerTokenCallback; import org.apache.yetus.audience.InterfaceAudience; @@ -163,12 +164,6 @@ private void setState(State state) { this.state = state; } - public static String[] mechanismNamesCompatibleWithPolicy(Map props) { - return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) - ? new String[] {} - : new String[] { OAUTHBEARER_MECHANISM}; - } - public static class OAuthBearerSaslClientFactory implements SaslClientFactory { @Override public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol, @@ -192,7 +187,7 @@ public SaslClient createSaslClient(String[] mechanisms, String authorizationId, @Override public String[] getMechanismNames(Map props) { - return OAuthBearerSaslClient.mechanismNamesCompatibleWithPolicy(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 index 1b941e6941e7..3ba721779e99 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer.internals; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +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; 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 index 5c27fa2776a4..65ef820696b6 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.provider; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import static org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerUtils.OAUTHBEARER_MECHANISM; import java.io.IOException; import java.net.InetAddress; import java.security.AccessController; 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 index da8687399948..d9e42d5973b4 100644 --- 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 @@ -36,7 +36,6 @@ @InterfaceAudience.Public public final class OAuthBearerTokenUtil { private static final Logger LOG = LoggerFactory.getLogger(OAuthBearerTokenUtil.class); - public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER"; public static final String TOKEN_KIND = "JWT_AUTH_TOKEN"; static { 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 index 2d5d225ef5db..caf85db44faa 100644 --- 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 @@ -17,6 +17,7 @@ */ 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; @@ -31,7 +32,6 @@ 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.security.token.OAuthBearerTokenUtil; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.junit.ClassRule; @@ -102,8 +102,7 @@ public void testWithPotentiallyMultipleTokens() throws Exception { createCallbackHandler() { OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler handler = new OAuthBearerSaslClientAuthenticationProvider.OAuthBearerSaslClientCallbackHandler(); - handler.configure(new Configuration(), OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM, - Collections.emptyMap()); + handler.configure(new Configuration(), OAUTHBEARER_MECHANISM, Collections.emptyMap()); return handler; } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java similarity index 56% rename from hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java rename to hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java index 0b3c10a8b0e9..19796e855b08 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerStringUtils.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/OAuthBearerUtils.java @@ -19,29 +19,23 @@ import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; +import javax.security.sasl.Sasl; import org.apache.yetus.audience.InterfaceAudience; -@InterfaceAudience.Public -public final class OAuthBearerStringUtils { +@InterfaceAudience.Private +public final class OAuthBearerUtils { + public static final String OAUTHBEARER_MECHANISM = "OAUTHBEARER"; + /** - * Converts a {@code Map} class into a string, concatenating keys and values - * Example: - * {@code mkString({ key: "hello", keyTwo: "hi" }, "|START|", "|END|", "=", ",") - * => "|START|key=hello,keyTwo=hi|END|"} + * Verifies configuration for OAuth Bearer authentication mechanism. + * Throws RuntimeException if PlainText is not allowed. */ - public static String mkString(Map map, String begin, String end, - String keyValueSeparator, String elementSeparator) { - StringBuilder bld = new StringBuilder(); - bld.append(begin); - String prefix = ""; - for (Map.Entry entry : map.entrySet()) { - bld.append(prefix).append(entry.getKey()). - append(keyValueSeparator).append(entry.getValue()); - prefix = elementSeparator; + 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."); } - bld.append(end); - return bld.toString(); + return new String[] { OAUTHBEARER_MECHANISM }; } /** @@ -66,17 +60,7 @@ public static Map parseMap(String mapStr, return map; } - /** - * Given two maps (A, B), returns all the key-value pairs in A whose keys are not contained in B - */ - public static Map subtractMap(Map minuend, - Map subtrahend) { - return minuend.entrySet().stream() - .filter(entry -> !subtrahend.containsKey(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - private OAuthBearerStringUtils() { + 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 index 7ed7d3081dd1..2bfd66a7bcaa 100644 --- 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 @@ -23,7 +23,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.security.sasl.SaslException; -import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; +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; @@ -65,7 +65,7 @@ public OAuthBearerClientInitialResponse(byte[] response) throws SaslException { String authzid = matcher.group("authzid"); this.authorizationId = authzid == null ? "" : authzid; String kvPairs = matcher.group("kvpairs"); - Map properties = OAuthBearerStringUtils.parseMap(kvPairs, "=", SEPARATOR); + 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"); 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 index 2b701fd3b735..c7c7446ac108 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer.internals; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +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; @@ -34,6 +34,7 @@ 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.OAuthBearerUtils; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; import org.apache.yetus.audience.InterfaceAudience; @@ -216,12 +217,6 @@ private void handleCallbackError(Exception e) throws SaslException { throw new SaslException(msg); } - public static String[] mechanismNamesCompatibleWithPolicy(Map props) { - return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) - ? new String[] {} - : new String[] { OAUTHBEARER_MECHANISM}; - } - public static class OAuthBearerSaslServerFactory implements SaslServerFactory { @Override public SaslServer createSaslServer(String mechanism, String protocol, String serverName, @@ -237,7 +232,7 @@ public SaslServer createSaslServer(String mechanism, String protocol, String ser @Override public String[] getMechanismNames(Map props) { - return OAuthBearerSaslServer.mechanismNamesCompatibleWithPolicy(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 index 2d7aeed149a2..99f7df223b45 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer.internals; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +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; 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 index 6a4418f75597..8fb33ef5a2f1 100644 --- 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 @@ -17,7 +17,7 @@ */ package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; -import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +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; From 1c5d9b25951ae3b85b02dbbac219df82d6b303f3 Mon Sep 17 00:00:00 2001 From: Andor Molnar Date: Thu, 20 Jan 2022 13:45:29 +0100 Subject: [PATCH 15/15] HBASE-26655. javac and checkstyle fixes --- .../internals/OAuthBearerSaslClient.java | 2 +- ...uthBearerSaslClientAuthenticationProvider.java | 15 ++++++++++----- .../internals/OAuthBearerSaslServer.java | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) 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 index fb5e8f3ff338..df3d08a081d1 100644 --- 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 @@ -33,9 +33,9 @@ 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.OAuthBearerUtils; 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; 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 index 65ef820696b6..915706c1027b 100644 --- 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 @@ -21,12 +21,14 @@ 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.Date; import java.util.Map; +import java.util.NavigableSet; import java.util.Set; -import java.util.SortedSet; import java.util.TreeSet; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; @@ -122,7 +124,7 @@ private OAuthBearerToken choosePrivateCredential(Set privateCr * 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. */ - SortedSet sortedByLifetime = + NavigableSet sortedByLifetime = new TreeSet<>( new Comparator() { @Override @@ -134,8 +136,11 @@ public int compare(OAuthBearerToken o1, OAuthBearerToken o2) { 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(), new Date(sortedByLifetime.first().lifetimeMs()), - new Date(sortedByLifetime.last().lifetimeMs())); + sortedByLifetime.size(), + LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.first().lifetimeMs()), + ZoneId.systemDefault()), + LocalDateTime.ofInstant(Instant.ofEpochMilli(sortedByLifetime.last().lifetimeMs()), + ZoneId.systemDefault())); } return sortedByLifetime.last(); } 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 index c7c7446ac108..3a56d5deb7c7 100644 --- 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 @@ -34,8 +34,8 @@ 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.OAuthBearerUtils; 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;