org.eclipse.hono.service.auth.device.CredentialsApiAuthProvider.java Source code

Java tutorial

Introduction

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

Source

/**
 * Copyright (c) 2017, 2018 Bosch Software Innovations GmbH and others.
 *
 * 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
 *    Red Hat Inc
 */

package org.eclipse.hono.service.auth.device;

import java.net.HttpURLConnection;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.hono.client.ClientErrorException;
import org.eclipse.hono.client.CredentialsClient;
import org.eclipse.hono.client.HonoClient;
import org.eclipse.hono.client.ServiceInvocationException;
import org.eclipse.hono.util.Constants;
import org.eclipse.hono.util.CredentialsConstants;
import org.eclipse.hono.util.CredentialsObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

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.ext.auth.User;
import io.vertx.ext.healthchecks.HealthCheckHandler;
import io.vertx.ext.healthchecks.Status;
import io.vertx.proton.ProtonConnection;

/**
 * A base class for implementing authentication providers that verify credentials provided by devices
 * against information on record retrieved using Hono's <em>Credentials</em> API.
 *
 */
public abstract class CredentialsApiAuthProvider implements HonoClientBasedAuthProvider {

    /**
     * A logger to be used by subclasses.
     */
    protected final Logger log = LoggerFactory.getLogger(getClass());
    private final Vertx vertx;
    private HonoClient credentialsClient;
    private AtomicBoolean shutdownInProgress = new AtomicBoolean(false);

    /**
     * Creates a new authentication provider for a vert.x instance.
     * 
     * @param vertx The vert.x instance to use for scheduling re-connection attempts.
     * @throws NullPointerException if vertx is {@code null}
     */
    protected CredentialsApiAuthProvider(final Vertx vertx) {
        this.vertx = Objects.requireNonNull(vertx);
    }

    /**
     * Sets the client to use for connecting to the Credentials service.
     *
     * @param credentialsServiceClient The client.
     * @throws NullPointerException if the client is {@code null}.
     */
    @Qualifier(CredentialsConstants.CREDENTIALS_ENDPOINT)
    @Autowired
    public final void setCredentialsServiceClient(final HonoClient credentialsServiceClient) {
        this.credentialsClient = Objects.requireNonNull(credentialsServiceClient);
    }

    /**
     * Registers a check that verifies connection to Hono's <em>Credentials</em>
     * service.
     */
    @Override
    public void registerReadinessChecks(final HealthCheckHandler readinessHandler) {
        readinessHandler.register("connected-to-credentials-service", status -> {
            Future<Boolean> checkResult = Optional.ofNullable(credentialsClient).map(client -> client.isConnected())
                    .orElse(Future.succeededFuture(Boolean.FALSE));
            checkResult.map(connected -> {
                if (connected) {
                    status.tryComplete(Status.OK());
                } else {
                    status.tryComplete(Status.KO());
                }
                return null;
            });
        });
    }

    /**
     * Does not do anything.
     */
    @Override
    public void registerLivenessChecks(final HealthCheckHandler livenessHandler) {
        // nothing to do
    }

    /**
     * Connects to the Credentials service using the configured client.
     */
    @Override
    public final Future<Void> start() {

        if (credentialsClient == null) {
            return Future.failedFuture(new IllegalStateException("Credentials service client is not set"));
        } else {
            return credentialsClient.connect(this::onDisconnectCredentialsService).map(connectedClient -> {
                log.info("connected to Credentials service");
                return (Void) null;
            }).recover(t -> {
                log.warn("connection to Credentials service failed", t);
                return Future.failedFuture(t);
            });
        }
    }

    /**
     * Attempts a reconnect for the Hono Credentials client after {@link Constants#DEFAULT_RECONNECT_INTERVAL_MILLIS} milliseconds.
     *
     * @param con The connection that was disconnected.
     */
    private void onDisconnectCredentialsService(final ProtonConnection con) {

        if (shutdownInProgress.get()) {
            log.debug("shut down in progress, not trying to re-connect to Credentials service");
        } else {
            vertx.setTimer(Constants.DEFAULT_RECONNECT_INTERVAL_MILLIS, reconnect -> {
                log.info("attempting to reconnect to Credentials service");
                credentialsClient.connect(this::onDisconnectCredentialsService).setHandler(connectAttempt -> {
                    if (connectAttempt.succeeded()) {
                        log.info("reconnected to Credentials service");
                    } else {
                        log.debug("cannot reconnect to Credentials service");
                    }
                });
            });
        }
    }

    /**
     * Closes the connection to the Credentials service.
     */
    @Override
    public final Future<Void> stop() {

        shutdownInProgress.set(true);
        Future<Void> result = Future.future();
        if (credentialsClient == null) {
            result.complete();
        } else {
            credentialsClient.shutdown(result.completer());
        }
        return result;
    }

    /**
     * Gets a client for the Credentials service.
     * 
     * @param tenantId The tenant to get the client for.
     * @return A future containing the client.
     */
    protected final Future<CredentialsClient> getCredentialsServiceClient(final String tenantId) {
        if (credentialsClient == null) {
            return Future.failedFuture(new IllegalStateException("no credentials client set"));
        } else {
            return credentialsClient.getOrCreateCredentialsClient(tenantId);
        }
    }

    /**
     * Retrieves credentials from the Credentials service.
     * 
     * @param deviceCredentials The credentials provided by the device.
     * @return A future containing the credentials on record as retrieved from
     *         Hono's <em>Credentials</em> API.
     * @throws NullPointerException if device credentials is {@code null}.
     */
    protected final Future<CredentialsObject> getCredentialsForDevice(final DeviceCredentials deviceCredentials) {

        Objects.requireNonNull(deviceCredentials);
        if (credentialsClient == null) {
            return Future.failedFuture(new IllegalStateException("Credentials API client is not set"));
        } else {
            return getCredentialsServiceClient(deviceCredentials.getTenantId())
                    .compose(client -> client.get(deviceCredentials.getType(), deviceCredentials.getAuthId()));
        }
    }

    @Override
    public final void authenticate(final DeviceCredentials deviceCredentials,
            final Handler<AsyncResult<Device>> resultHandler) {

        Objects.requireNonNull(deviceCredentials);
        Objects.requireNonNull(resultHandler);
        Future<Device> validationResult = Future.future();
        validationResult.setHandler(resultHandler);

        getCredentialsForDevice(deviceCredentials).recover(t -> {
            final ServiceInvocationException e = (ServiceInvocationException) t;
            if (e.getErrorCode() == HttpURLConnection.HTTP_NOT_FOUND) {
                return Future.failedFuture(
                        new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "bad credentials"));
            } else {
                return Future.failedFuture(t);
            }
        }).map(credentialsOnRecord -> {
            if (deviceCredentials.validate(credentialsOnRecord)) {
                return new Device(deviceCredentials.getTenantId(), credentialsOnRecord.getDeviceId());
            } else {
                throw new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "invalid credentials");
            }
        }).setHandler(resultHandler);
    }

    @Override
    public final void authenticate(final JsonObject authInfo, final Handler<AsyncResult<User>> resultHandler) {

        final DeviceCredentials credentials = getCredentials(Objects.requireNonNull(authInfo));
        if (credentials == null) {
            resultHandler.handle(Future.failedFuture(
                    new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "malformed credentials")));
        } else {
            authenticate(credentials, s -> {
                if (s.succeeded()) {
                    resultHandler.handle(Future.succeededFuture(s.result()));
                } else {
                    resultHandler.handle(Future.failedFuture(s.cause()));
                }
            });
        }
    }

    /**
     * Creates device credentials from authentication information provided by a
     * device.
     * <p>
     * Subclasses need to create a concrete {@code DeviceCredentials} instance based on
     * the information contained in the JSON object.
     * 
     * @param authInfo The credentials provided by the device.
     * @return The device credentials or {@code null} if the auth info does not contain
     *         the required information.
     * @throws NullPointerException if auth info is {@code null}.
     */
    protected abstract DeviceCredentials getCredentials(JsonObject authInfo);

}