All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.kafka.common.security.oauthbearer.internals.OAuthBearerSaslServer Maven / Gradle / Ivy

There is a newer version: 3.7.0
Show newest version
/*
 * 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.oauthbearer.internals;

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.kafka.common.errors.SaslAuthenticationException;
import org.apache.kafka.common.security.auth.SaslExtensions;
import org.apache.kafka.common.security.authenticator.SaslInternalConfigs;
import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerExtensionsValidatorCallback;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken;
import org.apache.kafka.common.security.oauthbearer.OAuthBearerValidatorCallback;
import org.apache.kafka.common.utils.Utils;
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).
 */
public class OAuthBearerSaslServer implements SaslServer {

    private static final Logger log = LoggerFactory.getLogger(OAuthBearerSaslServer.class);
    private static final String NEGOTIATED_PROPERTY_KEY_TOKEN = OAuthBearerLoginModule.OAUTHBEARER_MECHANISM + ".token";
    private static final String INTERNAL_ERROR_ON_SERVER = "Authentication could not be performed due to an internal error on the server";

    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 { if (response.length == 1 && response[0] == OAuthBearerSaslClient.BYTE_CONTROL_A && errorMessage != null) { log.debug("Received %x01 response from client after it received our error"); throw new SaslAuthenticationException(errorMessage); } errorMessage = null; OAuthBearerClientInitialResponse clientResponse; try { clientResponse = new OAuthBearerClientInitialResponse(response); } catch (SaslException e) { log.debug(e.getMessage()); throw e; } return process(clientResponse.tokenValue(), clientResponse.authorizationId(), clientResponse.extensions()); } @Override public String getAuthorizationID() { if (!complete) throw new IllegalStateException("Authentication exchange has not completed"); return tokenForNegotiatedProperty.principalName(); } @Override public String getMechanismName() { return OAuthBearerLoginModule.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 (SaslInternalConfigs.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.debug(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[] {OAuthBearerLoginModule.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 (int i = 0; i < mechanismNamesCompatibleWithPolicy.length; i++) { if (mechanismNamesCompatibleWithPolicy[i].equals(mechanism)) { return new OAuthBearerSaslServer(callbackHandler); } } return null; } @Override public String[] getMechanismNames(Map props) { return OAuthBearerSaslServer.mechanismNamesCompatibleWithPolicy(props); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy