org.eclipse.hono.server.HonoSaslAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.server.HonoSaslAuthenticator.java

Source

/**
 * Copyright (c) 2016 Bosch Software Innovations GmbH.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bosch Software Innovations GmbH - initial creation
 */
package org.eclipse.hono.server;

import static org.eclipse.hono.authentication.AuthenticationConstants.*;

import java.security.Principal;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.security.cert.X509Certificate;

import org.apache.qpid.proton.engine.Sasl;
import org.apache.qpid.proton.engine.Sasl.SaslOutcome;
import org.apache.qpid.proton.engine.Transport;
import org.eclipse.hono.util.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.DeliveryOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.NetSocket;
import io.vertx.proton.ProtonConnection;
import io.vertx.proton.sasl.ProtonSaslAuthenticator;

/**
 * A SASL authenticator that supports the PLAIN and EXTERNAL mechanisms for authenticating clients.
 * <p>
 * On successful authentication a {@code java.security.Principal} reflecting the client's
 * <em>authorization id</em> is attached to the {@code ProtonConnection} under key
 * {@link Constants#KEY_CLIENT_PRINCIPAL}.
 * <p>
 * Verification of credentials provided using PLAIN SASL is delegated to the {@code AuthenticationService}.
 * Client certificate validation is done by Vert.x' {@code NetServer} during the TLS handshake,
 * so this class merely extracts the <em>Common Name</em> (CN) field value from the client certificate's
 * <em>Subject DN</em> and uses it as the authorization id.
 */
public final class HonoSaslAuthenticator implements ProtonSaslAuthenticator {

    private static final Logger LOG = LoggerFactory.getLogger(HonoSaslAuthenticator.class);
    private static final Pattern PATTERN_CN = Pattern.compile("^CN=(.+?)(?:,\\s*[A-Z]{1,2}=.+|$)");
    private static final int AUTH_REQUEST_TIMEOUT_MILLIS = 300;
    private final Vertx vertx;
    private Sasl sasl;
    private boolean succeeded;
    private ProtonConnection protonConnection;
    private X509Certificate[] peerCertificateChain;

    /**
     * Creates a new authenticator for a Vertx environment.
     * 
     * @param vertx The Vert.x instance to run on.
     */
    public HonoSaslAuthenticator(final Vertx vertx) {
        this.vertx = Objects.requireNonNull(vertx);
    }

    @Override
    public void init(final NetSocket socket, final ProtonConnection protonConnection, final Transport transport) {
        LOG.debug("initializing SASL authenticator");
        this.protonConnection = protonConnection;
        this.sasl = transport.sasl();
        // TODO determine supported mechanisms dynamically based on registered AuthenticationService implementations
        sasl.server();
        sasl.allowSkip(false);
        sasl.setMechanisms(MECHANISM_EXTERNAL, MECHANISM_PLAIN);
        if (socket.isSsl()) {
            LOG.debug("client connected using TLS, extracting client certificate chain");
            try {
                peerCertificateChain = socket.peerCertificateChain();
                LOG.debug("found valid client certificate DN [{}]", peerCertificateChain[0].getSubjectDN());
            } catch (SSLPeerUnverifiedException e) {
                LOG.debug(
                        "could not extract client certificate chain, maybe TLS based client auth is not required");
            }
        }
    }

    @Override
    public void process(final Handler<Boolean> completionHandler) {

        String[] remoteMechanisms = sasl.getRemoteMechanisms();

        if (remoteMechanisms.length > 0) {
            String chosenMechanism = remoteMechanisms[0];
            LOG.debug("client wants to use {} SASL mechanism [host: {}, state: {}]", chosenMechanism,
                    sasl.getHostname(), sasl.getState().name());

            if (MECHANISM_PLAIN.equals(chosenMechanism)) {
                evaluatePlainResponse(completionHandler);
            } else if (MECHANISM_EXTERNAL.equals(chosenMechanism)) {
                evaluateExternalResponse(completionHandler);
            } else {
                LOG.info("client wants to use unsupported {} SASL mechanism [host: {}, state: {}]", chosenMechanism,
                        sasl.getHostname(), sasl.getState().name());
                sasl.done(SaslOutcome.PN_SASL_AUTH);
                completionHandler.handle(true);
            }
        } else {
            LOG.debug("client provided an empty list of SASL mechanisms [hostname: {}, state: {}]",
                    sasl.getHostname(), sasl.getState().name());
            completionHandler.handle(false);
        }
    }

    @Override
    public boolean succeeded() {
        return succeeded;
    }

    private void evaluateExternalResponse(final Handler<Boolean> completionHandler) {

        if (peerCertificateChain == null) {
            LOG.debug(
                    "SASL EXTERNAL authentication of client failed, client did not provide valid certificate chain");
            sasl.done(SaslOutcome.PN_SASL_AUTH);
        } else {
            Principal clientIdentity = peerCertificateChain[0].getSubjectDN();
            String commonName = getCommonName(clientIdentity.getName());
            if (commonName == null) {
                LOG.debug("SASL EXTERNAL authentication of client failed, could not determine authorization ID");
                sasl.done(SaslOutcome.PN_SASL_AUTH);
            } else {
                addPrincipal(commonName);
            }
        }
        completionHandler.handle(true);
    }

    private void evaluatePlainResponse(final Handler<Boolean> completionHandler) {

        byte[] saslResponse = new byte[sasl.pending()];
        sasl.recv(saslResponse, 0, saslResponse.length);

        final DeliveryOptions options = new DeliveryOptions().setSendTimeout(AUTH_REQUEST_TIMEOUT_MILLIS);
        final JsonObject authenticationRequest = getAuthenticationRequest(MECHANISM_PLAIN, saslResponse);
        vertx.eventBus().send(EVENT_BUS_ADDRESS_AUTHENTICATION_IN, authenticationRequest, options, reply -> {
            if (reply.succeeded()) {
                JsonObject result = (JsonObject) reply.result().body();
                LOG.debug("received result of successful authentication request: {}", result);
                addPrincipal(result.getString(FIELD_AUTHORIZATION_ID));
            } else {
                LOG.debug("authentication of client failed", reply.cause());
                sasl.done(SaslOutcome.PN_SASL_AUTH);
            }
            completionHandler.handle(true);
        });
    }

    private void addPrincipal(final String authzId) {
        addPrincipal(new Principal() {

            @Override
            public String getName() {
                return authzId;
            }
        });
    }

    private void addPrincipal(final Principal authzId) {

        succeeded = true;
        protonConnection.attachments().set(Constants.KEY_CLIENT_PRINCIPAL, Principal.class, authzId);
        LOG.debug("authentication of client [authorization ID: {}] succeeded", authzId.getName());
        sasl.done(SaslOutcome.PN_SASL_OK);
    }

    private static String getCommonName(final String subject) {
        Matcher matcher = PATTERN_CN.matcher(subject);
        if (matcher.matches()) {
            return matcher.group(1); // return CN field value
        } else {
            LOG.debug("could not extract common name from client certificate's subject DN");
            return null;
        }
    }
}