All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.eclipse.ditto.connectivity.service.messaging.httppush.AsyncJwtLoader Maven / Gradle / Ivy
/*
* Copyright (c) 2022 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.ditto.connectivity.service.messaging.httppush;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeExceptionBuilder;
import org.eclipse.ditto.base.service.UriEncoding;
import org.eclipse.ditto.connectivity.model.OAuthClientCredentials;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonFieldDefinition;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonRuntimeException;
import org.eclipse.ditto.jwt.model.ImmutableJsonWebToken;
import org.eclipse.ditto.jwt.model.JsonWebToken;
import org.eclipse.ditto.jwt.model.JwtInvalidException;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.http.javadsl.Http;
import akka.http.javadsl.model.ContentTypes;
import akka.http.javadsl.model.HttpEntities;
import akka.http.javadsl.model.HttpHeader;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.model.MediaRanges;
import akka.http.javadsl.model.MediaTypes;
import akka.http.javadsl.model.headers.Accept;
import akka.stream.Materializer;
import akka.stream.javadsl.Flow;
import akka.stream.javadsl.Sink;
import akka.stream.javadsl.Source;
import akka.util.ByteString;
import scala.util.Failure;
import scala.util.Success;
import scala.util.Try;
/**
* Implementation of AsyncCacheLoader for loading {@code JsonWebToken}s.
*/
final class AsyncJwtLoader implements AsyncCacheLoader {
private static final JsonFieldDefinition ACCESS_TOKEN =
JsonFactory.newStringFieldDefinition("access_token");
private static final HttpHeader ACCEPT_JSON = Accept.create(MediaRanges.create(MediaTypes.APPLICATION_JSON));
private final Flow, NotUsed> httpFlow;
private final Materializer materializer;
private final HttpRequest tokenRequest;
AsyncJwtLoader(final ActorSystem actorSystem, final OAuthClientCredentials credentials) {
this(actorSystem, buildHttpFlow(Http.get(actorSystem)), credentials);
}
AsyncJwtLoader(final ActorSystem actorSystem, final Flow, NotUsed> httpFlow,
final OAuthClientCredentials credentials) {
tokenRequest = toTokenRequest(credentials.getTokenEndpoint(), credentials.getClientId(),
credentials.getClientSecret(), credentials.getRequestedScopes(), credentials.getAudience().orElse(null));
materializer = Materializer.createMaterializer(actorSystem);
this.httpFlow = httpFlow;
}
@Override
public CompletableFuture asyncLoad(final String key, final Executor executor) {
return Source.single(tokenRequest)
.via(httpFlow)
.flatMapConcat(this::asJsonWebToken)
.runWith(Sink.head(), materializer).toCompletableFuture();
}
private Source asJsonWebToken(final Try tryResponse) {
if (tryResponse.isFailure()) {
return Source.failed(convertException(tryResponse.failed().get()));
} else {
return parseJwt(tryResponse.get());
}
}
private Source parseJwt(final HttpResponse response) {
final boolean areStatusAndContentTypeExpected = response.status().isSuccess() &&
response.entity().getContentType().equals(ContentTypes.APPLICATION_JSON);
if (areStatusAndContentTypeExpected) {
return response.entity()
.getDataBytes()
.fold(ByteString.emptyByteString(), ByteString::concat)
.map(ByteString::utf8String)
.flatMapConcat(this::extractJwt)
.mapMaterializedValue(any -> NotUsed.getInstance());
} else {
final String description = String.format("Response status is <%d> and content type is <%s>.",
response.status().intValue(), response.entity().getContentType());
return Source.failed(getJwtInvalidExceptionForResponse().description(description).build());
}
}
private Source extractJwt(final String body) {
try {
final var json = JsonObject.of(body);
return Source.single(ImmutableJsonWebToken.fromToken(json.getValueOrThrow(ACCESS_TOKEN)));
} catch (final NullPointerException | IllegalArgumentException | JsonRuntimeException |
DittoRuntimeException e) {
final JwtInvalidException jwtInvalid;
if (e instanceof JwtInvalidException jwtInvalidException) {
jwtInvalid = jwtInvalidException;
} else {
final var bodySummary = body.length() > 100 ? body.substring(0, 100) + "..." : body;
jwtInvalid = getJwtInvalidExceptionForResponse()
.description(String.format("Response body: <%s>", bodySummary))
.build();
}
return Source.failed(jwtInvalid);
}
}
private JwtInvalidException convertException(final Throwable error) {
if (error instanceof JwtInvalidException jwtInvalidException) {
return jwtInvalidException;
} else {
return JwtInvalidException.newBuilder()
.message(String.format("Request to token endpoint <%s> failed.", tokenRequest.getUri()))
.description(
String.format("Cause: %s: %s", error.getClass().getCanonicalName(), error.getMessage()))
.build();
}
}
private DittoRuntimeExceptionBuilder getJwtInvalidExceptionForResponse() {
return JwtInvalidException.newBuilder()
.message(String.format("Received invalid JSON web token response from <%s>.", tokenRequest.getUri()))
.description("Please verify that the token endpoint and client credentials are correct.");
}
private static HttpRequest toTokenRequest(final String tokenEndpoint,
final String clientId,
final String clientSecret,
final String scope,
@Nullable final String audience) {
final var body = asFormEncoded(clientId, clientSecret, scope, audience);
final var entity = HttpEntities.create(ContentTypes.APPLICATION_X_WWW_FORM_URLENCODED, body);
return HttpRequest.POST(tokenEndpoint).withEntity(entity).addHeader(ACCEPT_JSON);
}
private static String asFormEncoded(final String clientId, final String clientSecret, final String scope, @Nullable final String audience) {
if (audience == null) {
return String.format("grant_type=client_credentials" +
"&client_id=%s" +
"&client_secret=%s" +
"&scope=%s",
UriEncoding.encodeAllButUnreserved(clientId),
UriEncoding.encodeAllButUnreserved(clientSecret),
UriEncoding.encodeAllButUnreserved(scope)
);
}
return String.format("grant_type=client_credentials" +
"&client_id=%s" +
"&client_secret=%s" +
"&scope=%s" +
"&audience=%s",
UriEncoding.encodeAllButUnreserved(clientId),
UriEncoding.encodeAllButUnreserved(clientSecret),
UriEncoding.encodeAllButUnreserved(scope),
UriEncoding.encodeAllButUnreserved(audience)
);
}
private static Flow, NotUsed> buildHttpFlow(final Http http) {
return Flow.create()
.mapAsync(1, request -> http.singleRequest(request)
.>thenApply(Success::new)
.exceptionally(Failure::new)
);
}
}