joshelser commented on a change in pull request #3934: URL: https://github.com/apache/hbase/pull/3934#discussion_r779097090
########## File path: hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java ########## @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.shaded.json.JSONObject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code SaslServer} implementation for SASL/OAUTHBEARER in Kafka. An instance + * of {@link OAuthBearerToken} is available upon successful authentication via + * the negotiated property "{@code OAUTHBEARER.token}"; the token could be used + * in a custom authorizer (to authorize based on JWT claims rather than ACLs, + * for example). + */ [email protected] +public class OAuthBearerSaslServer implements SaslServer { + public static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslServer.class); + private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAUTHBEARER_MECHANISM + ".token"; + private static final String INTERNAL_ERROR_ON_SERVER = + "Authentication could not be performed due to an internal error on the server"; + static final String CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY = + "CREDENTIAL.LIFETIME.MS"; + + private final AuthenticateCallbackHandler callbackHandler; + + private boolean complete; + private OAuthBearerToken tokenForNegotiatedProperty = null; + private String errorMessage = null; + private SaslExtensions extensions; + + public OAuthBearerSaslServer(CallbackHandler callbackHandler) { Review comment: I think it might be better to fail hard (throw exception) if we try to construct the SaslServer in an inherently insecure manner. Then we could get rid of all of the indirection in this class about compatibility with mechanism/policy. ########## File path: hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java ########## @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.shaded.json.JSONObject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code SaslServer} implementation for SASL/OAUTHBEARER in Kafka. An instance + * of {@link OAuthBearerToken} is available upon successful authentication via + * the negotiated property "{@code OAUTHBEARER.token}"; the token could be used + * in a custom authorizer (to authorize based on JWT claims rather than ACLs, + * for example). + */ [email protected] +public class OAuthBearerSaslServer implements SaslServer { + public static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslServer.class); + private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAUTHBEARER_MECHANISM + ".token"; + private static final String INTERNAL_ERROR_ON_SERVER = + "Authentication could not be performed due to an internal error on the server"; + static final String CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY = + "CREDENTIAL.LIFETIME.MS"; + + private final AuthenticateCallbackHandler callbackHandler; + + private boolean complete; + private OAuthBearerToken tokenForNegotiatedProperty = null; + private String errorMessage = null; + private SaslExtensions extensions; + + public OAuthBearerSaslServer(CallbackHandler callbackHandler) { + if (!(callbackHandler instanceof AuthenticateCallbackHandler)) { + throw new IllegalArgumentException( + String.format("Callback handler must be castable to %s: %s", + AuthenticateCallbackHandler.class.getName(), callbackHandler.getClass().getName())); + } + this.callbackHandler = (AuthenticateCallbackHandler) callbackHandler; + } + + /** + * @throws SaslAuthenticationException + * if access token cannot be validated + * <p> + * <b>Note:</b> This method may throw + * {@link SaslAuthenticationException} to provide custom error + * messages to clients. But care should be taken to avoid including + * any information in the exception message that should not be + * leaked to unauthenticated clients. It may be safer to throw + * {@link SaslException} in some cases so that a standard error + * message is returned to clients. + * </p> + */ + @Override + public byte[] evaluateResponse(byte[] response) + throws SaslException, SaslAuthenticationException { + try { + if (response.length == 1 && response[0] == OAuthBearerSaslClient.BYTE_CONTROL_A && + errorMessage != null) { + LOG.error("Received %x01 response from client after it received our error"); + throw new SaslAuthenticationException(errorMessage); + } + errorMessage = null; + + OAuthBearerClientInitialResponse clientResponse; + clientResponse = new OAuthBearerClientInitialResponse(response); + + return process(clientResponse.tokenValue(), clientResponse.authorizationId(), + clientResponse.extensions()); + } catch (SaslAuthenticationException e) { + LOG.error("SASL authentication error", e); + throw e; + } catch (Exception e) { + LOG.error("SASL server problem", e); + throw e; + } + } + + @Override + public String getAuthorizationID() { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return tokenForNegotiatedProperty.principalName(); + } + + @Override + public String getMechanismName() { + return OAUTHBEARER_MECHANISM; + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + if (NEGOTIATED_PROPERTY_KEY_TOKEN.equals(propName)) { + return tokenForNegotiatedProperty; + } + if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { + return tokenForNegotiatedProperty.lifetimeMs(); + } + return extensions.map().get(propName); + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(incoming, offset, offset + len); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(outgoing, offset, offset + len); + } + + @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<String, String> 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<String, String> 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(), + OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), + "", "", ": ", "; ")); + LOG.debug(errorMessage); + throw new SaslAuthenticationException(errorMessage); + } + + return extensionsCallback.validatedExtensions(); + } + + private static String jsonErrorResponse(String errorStatus, String errorScope, + String errorOpenIDConfiguration) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("status", errorStatus); + if (!StringUtils.isBlank(errorScope)) { + jsonObject.put("scope", errorScope); + } + if (!StringUtils.isBlank(errorOpenIDConfiguration)) { + jsonObject.put("openid-configuration", errorOpenIDConfiguration); + } + return jsonObject.toJSONString(); + } + + private void handleCallbackError(Exception e) throws SaslException { + String msg = String.format("%s: %s", INTERNAL_ERROR_ON_SERVER, e.getMessage()); + LOG.debug(msg, e); + throw new SaslException(msg); + } + + public static String[] mechanismNamesCompatibleWithPolicy(Map<String, ?> props) { + return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) Review comment: `Boolean.valueOf(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT)))` is a little more succient. ########## File path: hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerSaslServer.java ########## @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals; + +import static org.apache.hadoop.hbase.security.token.OAuthBearerTokenUtil.OAUTHBEARER_MECHANISM; +import com.nimbusds.jose.shaded.json.JSONObject; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.security.sasl.SaslServerFactory; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.exceptions.SaslAuthenticationException; +import org.apache.hadoop.hbase.security.auth.AuthenticateCallbackHandler; +import org.apache.hadoop.hbase.security.auth.SaslExtensions; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerExtensionsValidatorCallback; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerStringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerValidatorCallback; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@code SaslServer} implementation for SASL/OAUTHBEARER in Kafka. An instance + * of {@link OAuthBearerToken} is available upon successful authentication via + * the negotiated property "{@code OAUTHBEARER.token}"; the token could be used + * in a custom authorizer (to authorize based on JWT claims rather than ACLs, + * for example). + */ [email protected] +public class OAuthBearerSaslServer implements SaslServer { + public static final Logger LOG = LoggerFactory.getLogger(OAuthBearerSaslServer.class); + private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAUTHBEARER_MECHANISM + ".token"; + private static final String INTERNAL_ERROR_ON_SERVER = + "Authentication could not be performed due to an internal error on the server"; + static final String CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY = + "CREDENTIAL.LIFETIME.MS"; + + private final AuthenticateCallbackHandler callbackHandler; + + private boolean complete; + private OAuthBearerToken tokenForNegotiatedProperty = null; + private String errorMessage = null; + private SaslExtensions extensions; + + public OAuthBearerSaslServer(CallbackHandler callbackHandler) { + if (!(callbackHandler instanceof AuthenticateCallbackHandler)) { + throw new IllegalArgumentException( + String.format("Callback handler must be castable to %s: %s", + AuthenticateCallbackHandler.class.getName(), callbackHandler.getClass().getName())); + } + this.callbackHandler = (AuthenticateCallbackHandler) callbackHandler; + } + + /** + * @throws SaslAuthenticationException + * if access token cannot be validated + * <p> + * <b>Note:</b> This method may throw + * {@link SaslAuthenticationException} to provide custom error + * messages to clients. But care should be taken to avoid including + * any information in the exception message that should not be + * leaked to unauthenticated clients. It may be safer to throw + * {@link SaslException} in some cases so that a standard error + * message is returned to clients. + * </p> + */ + @Override + public byte[] evaluateResponse(byte[] response) + throws SaslException, SaslAuthenticationException { + try { + if (response.length == 1 && response[0] == OAuthBearerSaslClient.BYTE_CONTROL_A && + errorMessage != null) { + LOG.error("Received %x01 response from client after it received our error"); + throw new SaslAuthenticationException(errorMessage); + } + errorMessage = null; + + OAuthBearerClientInitialResponse clientResponse; + clientResponse = new OAuthBearerClientInitialResponse(response); + + return process(clientResponse.tokenValue(), clientResponse.authorizationId(), + clientResponse.extensions()); + } catch (SaslAuthenticationException e) { + LOG.error("SASL authentication error", e); + throw e; + } catch (Exception e) { + LOG.error("SASL server problem", e); + throw e; + } + } + + @Override + public String getAuthorizationID() { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return tokenForNegotiatedProperty.principalName(); + } + + @Override + public String getMechanismName() { + return OAUTHBEARER_MECHANISM; + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + if (NEGOTIATED_PROPERTY_KEY_TOKEN.equals(propName)) { + return tokenForNegotiatedProperty; + } + if (CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) { + return tokenForNegotiatedProperty.lifetimeMs(); + } + return extensions.map().get(propName); + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(incoming, offset, offset + len); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) { + if (!complete) { + throw new IllegalStateException("Authentication exchange has not completed"); + } + return Arrays.copyOfRange(outgoing, offset, offset + len); + } + + @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<String, String> 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<String, String> 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(), + OAuthBearerStringUtils.mkString(extensionsCallback.invalidExtensions(), + "", "", ": ", "; ")); + LOG.debug(errorMessage); + throw new SaslAuthenticationException(errorMessage); + } + + return extensionsCallback.validatedExtensions(); + } + + private static String jsonErrorResponse(String errorStatus, String errorScope, + String errorOpenIDConfiguration) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("status", errorStatus); + if (!StringUtils.isBlank(errorScope)) { + jsonObject.put("scope", errorScope); + } + if (!StringUtils.isBlank(errorOpenIDConfiguration)) { + jsonObject.put("openid-configuration", errorOpenIDConfiguration); + } + return jsonObject.toJSONString(); + } + + private void handleCallbackError(Exception e) throws SaslException { + String msg = String.format("%s: %s", INTERNAL_ERROR_ON_SERVER, e.getMessage()); + LOG.debug(msg, e); + throw new SaslException(msg); + } + + public static String[] mechanismNamesCompatibleWithPolicy(Map<String, ?> props) { + return props != null && "true".equals(String.valueOf(props.get(Sasl.POLICY_NOPLAINTEXT))) Review comment: I'm also tryign to make sure I understand this check -- if we disallow plaintext mechanism, we also disallow oauth bearer mechanism? In the context of HBase, would we ever want to allow the user to run HBase with oauth-bearer authentication without wire encryption? For a production scenario, I think the answer would be "no". ########## File path: 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; + [email protected] +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<TokenIdentifier> secretManager, Map<String, String> 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<AttemptingUserProvidingSaslServer>() { + @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"); Review comment: In general, we use IOException as the base-class for caught exceptions. I'd suggest just throwing `InterruptedIOException` ########## File path: 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; - Review comment: nit: In general, try to avoid making changes to unrelated files. ########## File path: hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java ########## @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import java.text.ParseException; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Signed JWT implementation for OAuth Bearer authentication mech of SASL. + * + * This class is based on Kafka's Unsecured JWS token implementation. + */ [email protected] +public class OAuthBearerSignedJwt implements OAuthBearerToken { + private final String compactSerialization; + private final JWKSet jwkSet; + + private JWTClaimsSet claims; + private long lifetime; + private int maxClockSkewSeconds = 0; Review comment: Is the value of 0 disabling the clock skew validation? ########## File path: hbase-common/src/main/java/org/apache/hadoop/hbase/security/oauthbearer/internals/knox/OAuthBearerSignedJwt.java ########## @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.oauthbearer.internals.knox; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWSKeySelector; +import com.nimbusds.jose.proc.JWSVerificationKeySelector; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import java.text.ParseException; +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.security.oauthbearer.OAuthBearerToken; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Signed JWT implementation for OAuth Bearer authentication mech of SASL. + * + * This class is based on Kafka's Unsecured JWS token implementation. + */ [email protected] +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) { Review comment: Unless there's a reason, could we construct this via one constructor with all of the options or make a Builder class? (which could automatically validate when we construct the Token) ########## File path: pom.xml ########## @@ -1821,6 +1821,7 @@ <xz.version>1.9</xz.version> <zstd-jni.version>1.5.0-4</zstd-jni.version> <hbase-thirdparty.version>4.0.1</hbase-thirdparty.version> + <nimbusds.version>9.15</nimbusds.version> Review comment: Looks like they have 9.15.2 also released. I assume we would want to use the latest version. ########## File path: hbase-common/src/test/java/org/apache/hadoop/hbase/security/oauthbearer/internals/OAuthBearerClientInitialResponseTest.java ########## @@ -0,0 +1,144 @@ +/* + * 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.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 + */ + @Test + public void testBuildClientResponseToBytes() throws Exception { + String expectedMesssage = "n,,\u0001auth=Bearer 123.345.567\u0001nineteen=42\u0001\u0001"; Review comment: Are the `\u0001` characters in here from the serialization done in `OAuthBearerStringUtils`? Would be nice to use the corresponding API to generate this String. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
