org.eclipse.hono.service.auth.HonoSaslAuthenticator.java Source code

Java tutorial

Introduction

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

Source

/**
 * Copyright (c) 2016, 2017 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.service.auth;

import static org.eclipse.hono.service.auth.AuthenticationConstants.MECHANISM_EXTERNAL;
import static org.eclipse.hono.service.auth.AuthenticationConstants.MECHANISM_PLAIN;

import java.time.Duration;
import java.time.Instant;
import java.util.Objects;

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

import org.apache.qpid.proton.amqp.transport.AmqpError;
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.auth.HonoUser;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.JwtHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.NetSocket;
import io.vertx.proton.ProtonConnection;
import io.vertx.proton.ProtonHelper;
import io.vertx.proton.sasl.ProtonSaslAuthenticator;

/**
 * A SASL authenticator that supports the PLAIN and EXTERNAL mechanisms for authenticating clients.
 * <p>
 * On successful authentication a {@link HonoUser} reflecting the client's
 * <em>authorization id</em> and granted authorities is attached to the {@code ProtonConnection} under key
 * {@link Constants#KEY_CLIENT_PRINCIPAL}.
 * <p>
 * Verification of credentials is delegated to the {@code AuthenticationService} by means of sending a
 * message to {@link AuthenticationConstants#EVENT_BUS_ADDRESS_AUTHENTICATION_IN}.
 * <p>
 * Client certificate validation is done by Vert.x' {@code NetServer} during the TLS handshake,
 * so this class merely extracts the subject <em>Distinguished Name</em> (DN) from the client certificate
 * and uses it as the authorization id.
 */
public final class HonoSaslAuthenticator implements ProtonSaslAuthenticator {

    private static final Logger LOG = LoggerFactory.getLogger(HonoSaslAuthenticator.class);
    private final Vertx vertx;
    private final AuthenticationService authenticationService;
    private Sasl sasl;
    private boolean succeeded;
    private ProtonConnection protonConnection;
    private X509Certificate[] peerCertificateChain;

    /**
     * Creates a new authenticator.
     * 
     * @param vertx the Vertx environment to run on.
     * @param authService The service to use for authenticating client.
     * @throws NullPointerException if any of the parameters is {@code null}.
     */
    public HonoSaslAuthenticator(final Vertx vertx, final AuthenticationService authService) {
        this.vertx = Objects.requireNonNull(vertx);
        this.authenticationService = Objects.requireNonNull(authService);
    }

    @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) {
            LOG.debug("client provided an empty list of SASL mechanisms [hostname: {}, state: {}]",
                    sasl.getHostname(), sasl.getState().name());
            completionHandler.handle(false);
        } else {
            String chosenMechanism = remoteMechanisms[0];
            LOG.debug("client wants to authenticate using SASL [mechanism: {}, host: {}, state: {}]",
                    chosenMechanism, sasl.getHostname(), sasl.getState().name());

            Future<HonoUser> authTracker = Future.future();
            authTracker.setHandler(s -> {
                if (s.succeeded()) {

                    HonoUser user = s.result();
                    LOG.debug("authentication of client [authorization ID: {}] succeeded", user.getName());
                    Constants.setClientPrincipal(protonConnection, user);
                    succeeded = true;
                    registerTimerForHandlingExpiredToken(user, protonConnection);
                    sasl.done(SaslOutcome.PN_SASL_OK);

                } else {

                    LOG.debug("authentication failed: " + s.cause().getMessage());
                    sasl.done(SaslOutcome.PN_SASL_AUTH);

                }
                completionHandler.handle(Boolean.TRUE);
            });

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

            verify(chosenMechanism, saslResponse, authTracker.completer());
        }
    }

    // We currently have no way of refreshing the token before expiration using
    // vertx-proton's existing API. We therefore force the client to re-connect when
    // the token expires.
    // Once we are able to refresh tokens using vertx-proton API we will get rid
    // of this ugly hack.
    // TODO refresh tokens properly
    private void registerTimerForHandlingExpiredToken(final HonoUser user, final ProtonConnection con) {

        if (user.getToken() != null) {
            Duration expiration = Duration.between(Instant.now(),
                    JwtHelper.getExpiration(user.getToken()).toInstant());
            vertx.setTimer(expiration.toMillis(), tid -> {
                LOG.debug("client's [{}] access token has expired, closing connection", user.getName());
                con.setCondition(ProtonHelper.condition(AmqpError.UNAUTHORIZED_ACCESS, "access token expired"))
                        .close();
                String conId = con.attachments().get(Constants.KEY_CONNECTION_ID, String.class);
                if (conId != null) {
                    vertx.eventBus().publish(Constants.EVENT_BUS_ADDRESS_CONNECTION_CLOSED, conId);
                }
            });
        }
    }

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

    private void verify(final String mechanism, final byte[] saslResponse,
            final Handler<AsyncResult<HonoUser>> authResultHandler) {

        JsonObject authRequest = AuthenticationConstants.getAuthenticationRequest(mechanism, saslResponse);
        if (peerCertificateChain != null) {
            authRequest.put(AuthenticationConstants.FIELD_SUBJECT_DN,
                    peerCertificateChain[0].getSubjectDN().getName());
        }
        authenticationService.authenticate(authRequest, authResultHandler);
    }
}