/******************************************************************************* * Copyright (c) 2016, 2019 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 *******************************************************************************/ package org.eclipse.hono.client; import java.net.HttpURLConnection; import java.util.Objects; import java.util.Optional; import javax.security.sasl.AuthenticationException; import org.eclipse.hono.auth.HonoUser; import org.eclipse.hono.auth.HonoUserAdapter; import org.eclipse.hono.connection.ConnectionFactory; import org.eclipse.hono.util.AuthenticationConstants; import org.eclipse.hono.util.MessageHelper; 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.Promise; import io.vertx.core.Vertx; import io.vertx.proton.ProtonClientOptions; import io.vertx.proton.ProtonConnection; import io.vertx.proton.ProtonMessageHandler; import io.vertx.proton.ProtonReceiver; import io.vertx.proton.sasl.MechanismMismatchException; /** * A client for retrieving a token from an authentication service via AMQP 1.0. * */ public final class AuthenticationServerClient { private static final Logger LOG = LoggerFactory.getLogger(AuthenticationServerClient.class); private final ConnectionFactory factory; private final Vertx vertx; /** * Creates a client for a remote authentication server. * * @param vertx The Vert.x instance to run on. * @param connectionFactory The factory. * @throws NullPointerException if any of the parameters is {@code null}. */ public AuthenticationServerClient( final Vertx vertx, final ConnectionFactory connectionFactory) { this.vertx = Objects.requireNonNull(vertx); this.factory = Objects.requireNonNull(connectionFactory); } /** * Verifies a Subject DN with a remote authentication server using SASL EXTERNAL. * <p> * This method currently always fails the handler because there is no way (yet) in vertx-proton * to perform a SASL EXTERNAL exchange including an authorization id. * * @param authzid The identity to act as. * @param subjectDn The Subject DN. * @param authenticationResultHandler The handler to invoke with the authentication result. On successful authentication, * the result contains a JWT with the authenticated user's claims. */ public void verifyExternal(final String authzid, final String subjectDn, final Handler<AsyncResult<HonoUser>> authenticationResultHandler) { // unsupported mechanism (until we get better control over client SASL params in vertx-proton) authenticationResultHandler.handle(Future .failedFuture(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "unsupported mechanism"))); } /** * Verifies username/password credentials with a remote authentication server using SASL PLAIN. * * @param authzid The identity to act as. * @param authcid The username. * @param password The password. * @param authenticationResultHandler The handler to invoke with the authentication result. On successful authentication, * the result contains a JWT with the authenticated user's claims. */ public void verifyPlain(final String authzid, final String authcid, final String password, final Handler<AsyncResult<HonoUser>> authenticationResultHandler) { final ProtonClientOptions options = new ProtonClientOptions(); options.setReconnectAttempts(3).setReconnectInterval(50); options.addEnabledSaslMechanism(AuthenticationConstants.MECHANISM_PLAIN); final Promise<ProtonConnection> connectAttempt = Promise.promise(); factory.connect(options, authcid, password, null, null, connectAttempt); connectAttempt.future() .compose(openCon -> getToken(openCon)) .onComplete(s -> { if (s.succeeded()) { authenticationResultHandler.handle(Future.succeededFuture(s.result())); } else { authenticationResultHandler .handle(Future.failedFuture(mapConnectionFailureToServiceInvocationException(s.cause()))); } Optional.ofNullable(connectAttempt.future().result()) .ifPresent(con -> { LOG.debug("closing connection to Authentication service"); con.close(); }); }); } private ServiceInvocationException mapConnectionFailureToServiceInvocationException(final Throwable connectionFailureCause) { final ServiceInvocationException exception; if (connectionFailureCause == null) { exception = new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "failed to connect to Authentication service"); } else if (connectionFailureCause instanceof AuthenticationException) { exception = new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "failed to authenticate with Authentication service"); } else if (connectionFailureCause instanceof MechanismMismatchException) { exception = new ClientErrorException(HttpURLConnection.HTTP_UNAUTHORIZED, "Authentication service does not support SASL mechanism"); } else { exception = new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "failed to connect to Authentication service", connectionFailureCause); } return exception; } private Future<HonoUser> getToken(final ProtonConnection openCon) { final Promise<HonoUser> result = Promise.promise(); final ProtonMessageHandler messageHandler = (delivery, message) -> { final String type = MessageHelper.getApplicationProperty( message.getApplicationProperties(), AuthenticationConstants.APPLICATION_PROPERTY_TYPE, String.class); if (AuthenticationConstants.TYPE_AMQP_JWT.equals(type)) { final String payload = MessageHelper.getPayloadAsString(message); if (payload != null) { final HonoUser user = new HonoUserAdapter() { @Override public String getToken() { return payload; } }; LOG.debug("successfully retrieved token from Authentication service"); result.complete(user); } else { result.fail(new ServerErrorException(HttpURLConnection.HTTP_INTERNAL_ERROR, "message from Authentication service contains no body")); } } else { result.fail(new ServerErrorException(HttpURLConnection.HTTP_INTERNAL_ERROR, "Authentication service issued unsupported token [type: " + type + "]")); } }; openReceiver(openCon, messageHandler) .onComplete(attempt -> { if (attempt.succeeded()) { vertx.setTimer(5000, tid -> { result.tryFail(new ServerErrorException(HttpURLConnection.HTTP_UNAVAILABLE, "time out reached while waiting for token from Authentication service")); }); LOG.debug("opened receiver link to Authentication service, waiting for token ..."); } else { result.fail(attempt.cause()); } }); return result.future(); } private static Future<ProtonReceiver> openReceiver(final ProtonConnection openConnection, final ProtonMessageHandler messageHandler) { final Promise<ProtonReceiver> result = Promise.promise(); final ProtonReceiver recv = openConnection.createReceiver(AuthenticationConstants.ENDPOINT_NAME_AUTHENTICATION); recv.openHandler(result); recv.handler(messageHandler); recv.open(); return result.future(); } }