| /* |
| * 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.kafka.common.security.scram.internals; |
| |
| import java.security.InvalidKeyException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.security.auth.callback.Callback; |
| import javax.security.auth.callback.CallbackHandler; |
| import javax.security.auth.callback.NameCallback; |
| import javax.security.sasl.SaslException; |
| import javax.security.sasl.SaslServer; |
| import javax.security.sasl.SaslServerFactory; |
| |
| import org.apache.kafka.common.errors.AuthenticationException; |
| import org.apache.kafka.common.errors.IllegalSaslStateException; |
| import org.apache.kafka.common.errors.SaslAuthenticationException; |
| import org.apache.kafka.common.security.authenticator.SaslInternalConfigs; |
| import org.apache.kafka.common.security.scram.ScramCredential; |
| import org.apache.kafka.common.security.scram.ScramCredentialCallback; |
| import org.apache.kafka.common.security.scram.ScramLoginModule; |
| import org.apache.kafka.common.security.scram.internals.ScramMessages.ClientFinalMessage; |
| import org.apache.kafka.common.security.scram.internals.ScramMessages.ClientFirstMessage; |
| import org.apache.kafka.common.security.scram.internals.ScramMessages.ServerFinalMessage; |
| import org.apache.kafka.common.security.scram.internals.ScramMessages.ServerFirstMessage; |
| import org.apache.kafka.common.security.token.delegation.internals.DelegationTokenCredentialCallback; |
| import org.apache.kafka.common.utils.Utils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * SaslServer implementation for SASL/SCRAM. This server is configured with a callback |
| * handler for integration with a credential manager. Kafka brokers provide callbacks |
| * based on a Zookeeper-based password store. |
| * |
| * @see <a href="https://tools.ietf.org/html/rfc5802">RFC 5802</a> |
| */ |
| public class ScramSaslServer implements SaslServer { |
| |
| private static final Logger log = LoggerFactory.getLogger(ScramSaslServer.class); |
| private static final Set<String> SUPPORTED_EXTENSIONS = Utils.mkSet(ScramLoginModule.TOKEN_AUTH_CONFIG); |
| |
| enum State { |
| RECEIVE_CLIENT_FIRST_MESSAGE, |
| RECEIVE_CLIENT_FINAL_MESSAGE, |
| COMPLETE, |
| FAILED |
| } |
| |
| private final ScramMechanism mechanism; |
| private final ScramFormatter formatter; |
| private final CallbackHandler callbackHandler; |
| private State state; |
| private String username; |
| private ClientFirstMessage clientFirstMessage; |
| private ServerFirstMessage serverFirstMessage; |
| private ScramExtensions scramExtensions; |
| private ScramCredential scramCredential; |
| private String authorizationId; |
| private Long tokenExpiryTimestamp; |
| |
| public ScramSaslServer(ScramMechanism mechanism, Map<String, ?> props, CallbackHandler callbackHandler) throws NoSuchAlgorithmException { |
| this.mechanism = mechanism; |
| this.formatter = new ScramFormatter(mechanism); |
| this.callbackHandler = callbackHandler; |
| setState(State.RECEIVE_CLIENT_FIRST_MESSAGE); |
| } |
| |
| /** |
| * @throws SaslAuthenticationException if the requested authorization id is not the same as username. |
| * <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 |
| * most cases so that a standard error message is returned to clients. |
| * </p> |
| */ |
| @Override |
| public byte[] evaluateResponse(byte[] response) throws SaslException, SaslAuthenticationException { |
| try { |
| switch (state) { |
| case RECEIVE_CLIENT_FIRST_MESSAGE: |
| this.clientFirstMessage = new ClientFirstMessage(response); |
| this.scramExtensions = clientFirstMessage.extensions(); |
| if (!SUPPORTED_EXTENSIONS.containsAll(scramExtensions.map().keySet())) { |
| log.debug("Unsupported extensions will be ignored, supported {}, provided {}", |
| SUPPORTED_EXTENSIONS, scramExtensions.map().keySet()); |
| } |
| String serverNonce = formatter.secureRandomString(); |
| try { |
| String saslName = clientFirstMessage.saslName(); |
| this.username = formatter.username(saslName); |
| NameCallback nameCallback = new NameCallback("username", username); |
| ScramCredentialCallback credentialCallback; |
| if (scramExtensions.tokenAuthenticated()) { |
| DelegationTokenCredentialCallback tokenCallback = new DelegationTokenCredentialCallback(); |
| credentialCallback = tokenCallback; |
| callbackHandler.handle(new Callback[]{nameCallback, tokenCallback}); |
| if (tokenCallback.tokenOwner() == null) |
| throw new SaslException("Token Authentication failed: Invalid tokenId : " + username); |
| this.authorizationId = tokenCallback.tokenOwner(); |
| this.tokenExpiryTimestamp = tokenCallback.tokenExpiryTimestamp(); |
| } else { |
| credentialCallback = new ScramCredentialCallback(); |
| callbackHandler.handle(new Callback[]{nameCallback, credentialCallback}); |
| this.authorizationId = username; |
| this.tokenExpiryTimestamp = null; |
| } |
| this.scramCredential = credentialCallback.scramCredential(); |
| if (scramCredential == null) |
| throw new SaslException("Authentication failed: Invalid user credentials"); |
| String authorizationIdFromClient = clientFirstMessage.authorizationId(); |
| if (!authorizationIdFromClient.isEmpty() && !authorizationIdFromClient.equals(username)) |
| throw new SaslAuthenticationException("Authentication failed: Client requested an authorization id that is different from username"); |
| |
| if (scramCredential.iterations() < mechanism.minIterations()) |
| throw new SaslException("Iterations " + scramCredential.iterations() + " is less than the minimum " + mechanism.minIterations() + " for " + mechanism); |
| this.serverFirstMessage = new ServerFirstMessage(clientFirstMessage.nonce(), |
| serverNonce, |
| scramCredential.salt(), |
| scramCredential.iterations()); |
| setState(State.RECEIVE_CLIENT_FINAL_MESSAGE); |
| return serverFirstMessage.toBytes(); |
| } catch (SaslException | AuthenticationException e) { |
| throw e; |
| } catch (Throwable e) { |
| throw new SaslException("Authentication failed: Credentials could not be obtained", e); |
| } |
| |
| case RECEIVE_CLIENT_FINAL_MESSAGE: |
| try { |
| ClientFinalMessage clientFinalMessage = new ClientFinalMessage(response); |
| verifyClientProof(clientFinalMessage); |
| byte[] serverKey = scramCredential.serverKey(); |
| byte[] serverSignature = formatter.serverSignature(serverKey, clientFirstMessage, serverFirstMessage, clientFinalMessage); |
| ServerFinalMessage serverFinalMessage = new ServerFinalMessage(null, serverSignature); |
| clearCredentials(); |
| setState(State.COMPLETE); |
| return serverFinalMessage.toBytes(); |
| } catch (InvalidKeyException e) { |
| throw new SaslException("Authentication failed: Invalid client final message", e); |
| } |
| |
| default: |
| throw new IllegalSaslStateException("Unexpected challenge in Sasl server state " + state); |
| } |
| } catch (SaslException | AuthenticationException e) { |
| clearCredentials(); |
| setState(State.FAILED); |
| throw e; |
| } |
| } |
| |
| @Override |
| public String getAuthorizationID() { |
| if (!isComplete()) |
| throw new IllegalStateException("Authentication exchange has not completed"); |
| return authorizationId; |
| } |
| |
| @Override |
| public String getMechanismName() { |
| return mechanism.mechanismName(); |
| } |
| |
| @Override |
| public Object getNegotiatedProperty(String propName) { |
| if (!isComplete()) |
| throw new IllegalStateException("Authentication exchange has not completed"); |
| if (SaslInternalConfigs.CREDENTIAL_LIFETIME_MS_SASL_NEGOTIATED_PROPERTY_KEY.equals(propName)) |
| return tokenExpiryTimestamp; // will be null if token not used |
| if (SUPPORTED_EXTENSIONS.contains(propName)) |
| return scramExtensions.map().get(propName); |
| else |
| return null; |
| } |
| |
| @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 void dispose() { |
| } |
| |
| private void setState(State state) { |
| log.debug("Setting SASL/{} server state to {}", mechanism, state); |
| this.state = state; |
| } |
| |
| private void verifyClientProof(ClientFinalMessage clientFinalMessage) throws SaslException { |
| try { |
| byte[] expectedStoredKey = scramCredential.storedKey(); |
| byte[] clientSignature = formatter.clientSignature(expectedStoredKey, clientFirstMessage, serverFirstMessage, clientFinalMessage); |
| byte[] computedStoredKey = formatter.storedKey(clientSignature, clientFinalMessage.proof()); |
| if (!MessageDigest.isEqual(computedStoredKey, expectedStoredKey)) |
| throw new SaslException("Invalid client credentials"); |
| } catch (InvalidKeyException e) { |
| throw new SaslException("Sasl client verification failed", e); |
| } |
| } |
| |
| private void clearCredentials() { |
| scramCredential = null; |
| clientFirstMessage = null; |
| serverFirstMessage = null; |
| } |
| |
| public static class ScramSaslServerFactory implements SaslServerFactory { |
| |
| @Override |
| public SaslServer createSaslServer(String mechanism, String protocol, String serverName, Map<String, ?> props, CallbackHandler cbh) |
| throws SaslException { |
| |
| if (!ScramMechanism.isScram(mechanism)) { |
| throw new SaslException(String.format("Requested mechanism '%s' is not supported. Supported mechanisms are '%s'.", |
| mechanism, ScramMechanism.mechanismNames())); |
| } |
| try { |
| return new ScramSaslServer(ScramMechanism.forMechanismName(mechanism), props, cbh); |
| } catch (NoSuchAlgorithmException e) { |
| throw new SaslException("Hash algorithm not supported for mechanism " + mechanism, e); |
| } |
| } |
| |
| @Override |
| public String[] getMechanismNames(Map<String, ?> props) { |
| Collection<String> mechanisms = ScramMechanism.mechanismNames(); |
| return mechanisms.toArray(new String[mechanisms.size()]); |
| } |
| } |
| } |