diff --git a/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java b/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java index 5961799..209b9c4 100644 --- a/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java +++ b/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java @@ -77,12 +77,12 @@ public void initCache(Caffeine caffeine) { /** * Clears the cached {@link CryptoContext} instances. - * + *

* This method invalidates all entries in the cache, ensuring that any * cached cryptographic contexts are removed. Subsequent operations * will no longer utilize the invalidated contexts and may trigger * re-creation of new contexts as needed. - * + *

* If the cache is uninitialized or null, this method has no effect. */ public void clearCache() { @@ -95,22 +95,22 @@ private CryptoContext getContext(Id id) throws CryptoException { } /** - * Performs one-shot encryption of the given data for the specified recipient. + * Performs one-shot encryption of the given data for the specified receiver. *

- * This operation leverages a cached {@link CryptoContext} instance associated with the recipient, + * This operation leverages a cached {@link CryptoContext} instance associated with the receiver, * reducing the overhead of repeatedly computing cryptographic contexts. * - * @param recipient the recipient's {@link Id}; must not be {@code null} + * @param receiver the receiver's {@link Id}; must not be {@code null} * @param data the plaintext data to encrypt; must not be {@code null} * @return the encrypted data including the nonce prepended - * @throws NullPointerException if {@code recipient} or {@code data} is {@code null} + * @throws NullPointerException if {@code receiver} or {@code data} is {@code null} * @throws CryptoException if an error occurs during encryption */ @Override - public byte[] encrypt(Id recipient, byte[] data) throws CryptoException { - Objects.requireNonNull(recipient, "recipient"); + public byte[] encrypt(Id receiver, byte[] data) throws CryptoException { + Objects.requireNonNull(receiver, "receiver"); Objects.requireNonNull(data, "data"); - return getContext(recipient).encrypt(data); + return getContext(receiver).encrypt(data); } /** diff --git a/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java b/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java index e65ece3..62a6377 100644 --- a/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java +++ b/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java @@ -85,23 +85,23 @@ public boolean verify(byte[] data, byte[] signature) { } /** - * Performs one-shot encryption of the given data for the specified recipient. + * Performs one-shot encryption of the given data for the specified receiver. * - * @param recipient the recipient's {@link Id}; must not be {@code null} + * @param receiver the receiver's {@link Id}; must not be {@code null} * @param data the plaintext data to encrypt; must not be {@code null} * @return the encrypted data including the nonce prepended - * @throws NullPointerException if {@code recipient} or {@code data} is {@code null} + * @throws NullPointerException if {@code receiver} or {@code data} is {@code null} * @throws CryptoException if an error occurs during encryption */ @Override - public byte[] encrypt(Id recipient, byte[] data) throws CryptoException { - Objects.requireNonNull(recipient, "recipient"); + public byte[] encrypt(Id receiver, byte[] data) throws CryptoException { + Objects.requireNonNull(receiver, "receiver"); Objects.requireNonNull(data, "data"); try { // TODO: how to avoid the memory copy?! CryptoBox.Nonce nonce = CryptoBox.Nonce.random(); - CryptoBox.PublicKey pk = recipient.toEncryptionKey(); + CryptoBox.PublicKey pk = receiver.toEncryptionKey(); CryptoBox.PrivateKey sk = encryptionKeyPair.privateKey(); byte[] cipher = CryptoBox.encrypt(data, pk, sk, nonce); diff --git a/api/src/main/java/io/bosonnetwork/service/Federation.java b/api/src/main/java/io/bosonnetwork/service/Federation.java index 67a7f94..73cf51a 100644 --- a/api/src/main/java/io/bosonnetwork/service/Federation.java +++ b/api/src/main/java/io/bosonnetwork/service/Federation.java @@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture; import io.bosonnetwork.Id; +import io.bosonnetwork.web.CompactWebTokenAuth; /** * Federation manager (read-only) interface for the Boson Super Node services. @@ -35,6 +36,22 @@ * specific services hosted by federated nodes. */ public interface Federation { + /** + * Represents the type of incident that can occur within the federation. + * This enumeration is used to classify and report issues related to + * federated nodes or services. + */ + enum IncidentType { + /** Indicates a complete failure or unavailability of a service. */ + SERVICE_OUTAGE, + /** Indicates a service encountering an operation failure or error. */ + SERVICE_ERROR, + /** Indicates a poorly formed or invalid request received. */ + MALFORMED_REQUEST, + /** Indicates an invalid or improperly constructed response sent. */ + MALFORMED_RESPONSE + } + /** * Retrieves a federated node by its ID. * @@ -43,7 +60,7 @@ public interface Federation { * @return a {@link CompletableFuture} that completes with the {@link FederatedNode} object if found, * or completes exceptionally/with null if the node cannot be found or federated */ - public CompletableFuture getNode(Id nodeId, boolean federateIfNotExists); + CompletableFuture getNode(Id nodeId, boolean federateIfNotExists); /** * Retrieves a federated node by its ID. @@ -65,7 +82,7 @@ default CompletableFuture getNode(Id nodeId) { * @return a {@link CompletableFuture} that completes with {@code true} if the node exists, * or {@code false} otherwise */ - public CompletableFuture existsNode(Id nodeId); + CompletableFuture existsNode(Id nodeId); /** * Retrieves information about a specific service hosted by a federated node. @@ -75,7 +92,7 @@ default CompletableFuture getNode(Id nodeId) { * @return a {@link CompletableFuture} that completes with the list of {@link ServiceInfo} if found, * or completes exceptionally/with null if the service cannot be located */ - public CompletableFuture> getServices(Id peerId, Id nodeId); + CompletableFuture> getServices(Id peerId, Id nodeId); /** * Retrieves a list of services associated with a specific peer identified by its ID. @@ -85,5 +102,34 @@ default CompletableFuture getNode(Id nodeId) { * representing the services associated with the specified peer, or completes exceptionally * if an error occurs while retrieving the services */ - public CompletableFuture> getServices(Id peerId); + CompletableFuture> getServices(Id peerId); + + /** + * Reports an incident associated with a specific federated node and peer. + * + * @param nodeId the unique identifier of the federated node where the incident occurred + * @param peerId the unique identifier of the peer involved in the incident + * @param incident the type of incident being reported + * @param details a detailed description of the incident + * @return a {@link CompletableFuture} that completes when the incident has been reported successfully, + * or completes exceptionally if an error occurs during the reporting process + */ + CompletableFuture reportIncident(Id nodeId, Id peerId, IncidentType incident, String details); + + /** + * Retrieves the instance of {@link FederationAuthenticator} associated with this federation. + * + * @return the {@link FederationAuthenticator} responsible for managing authentication + * within the federation context. + */ + FederationAuthenticator getAuthenticator(); + + /** + * Retrieves the instance of {@link CompactWebTokenAuth} used for handling + * web token authentication within the federation. + * + * @return the {@link CompactWebTokenAuth} instance responsible for managing + * web token authentication. + */ + CompactWebTokenAuth getWebTokenAuthenticator(); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java index 572f205..9aea2f0 100644 --- a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java +++ b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java @@ -57,7 +57,7 @@ * {@link #undeploy()}. This ensures forward compatibility with Vert.x 5’s {@code VerticleBase}. *

*/ -public abstract class BosonVerticle implements /* Verticle, */ Deployable { +public abstract class BosonVerticle implements Deployable { /** * Reference to the Vert.x instance that deployed this verticle */ @@ -82,7 +82,7 @@ public final Vertx getVertx() { * * @return the Vert.x context */ - public final Context vertxContext() { + protected final Context vertxContext() { return vertxContext; } @@ -103,96 +103,52 @@ public final String deploymentID() { * * @return the configuration as a {@link JsonObject} */ - public final JsonObject vertxConfig() { + protected final JsonObject vertxConfig() { return vertxContext.config(); } /** - * Initializes the Verticle. + * Prepares this verticle for deployment. *

- * This method is called by Vert.x when the Verticle instance is deployed. - * User code should not call this directly. - *

- * - * @param vertx the Vert.x instance - * @param context the context associated with this Verticle - */ - public final void init(Vertx vertx, Context context) { - prepare(vertx, context); - } - - /** - * Prepares this verticle for execution. - *

- * This method is invoked internally by {@link #init(Vertx, Context)} and can be overridden + * This method is invoked internally by {@link #deploy(Context)} and can be overridden * if additional setup is needed before deployment. *

* * @param vertx the Vert.x instance * @param context the Vert.x context */ - public void prepare(Vertx vertx, Context context) { + protected void prepare(Vertx vertx, Context context) { this.vertx = vertx; this.vertxContext = context; } /** - * Called when the Verticle is started. - *

- * This implementation delegates to {@link #deploy()}, which should return a {@link Future} - * that completes when startup is done. - *

- * - * @param startPromise a promise that should be completed when startup is done - * @throws Exception if startup fails - */ - public final void start(Promise startPromise) throws Exception { - deploy().onComplete(startPromise); - } - - /** - * Called when the Verticle is stopped. - *

- * This implementation delegates to {@link #undeploy()}, which should return a {@link Future} - * that completes when shutdown is done. - *

- * - * @param stopPromise a promise that should be completed when shutdown is done - * @throws Exception if shutdown fails - */ - public final void stop(Promise stopPromise) throws Exception { - undeploy().onComplete(stopPromise); - } - - /** - * Called during startup to perform asynchronous initialization logic. + * Called during deployment to perform asynchronous initialization logic. *

- * This method is invoked by {@link #start(Promise)}. + * This method is invoked by {@link #deploy(Context)}. *

* * @return a future that completes when setup is finished */ - public abstract Future deploy(); + protected abstract Future deploy(); /** - * Called during shutdown to perform asynchronous cleanup logic. + * Called during undeployment to perform asynchronous cleanup logic. *

- * This method is invoked by {@link #stop(Promise)}. + * This method is invoked by {@link #undeploy(Context)}. *

* * @return a future that completes when teardown is finished */ - public abstract Future undeploy(); + protected abstract Future undeploy(); /** - * Internal helper method to simulate deployment under Vert.x 5.x’s {@code Deployable} interface. + * Start the deployable. *

- * This should not be called directly by user code. It is used by Vert.x internals or - * integration layers that work with Vert.x 5.x’s deployment model. - *

+ * Vert.x calls this method when deploying this deployable. You do not call it yourself. * - * @param context the Vert.x context - * @return a future that completes when deployment is finished + * @param context the Vert.x context assigned to this deployable + * @return a future signaling the start-up completion * @throws Exception if deployment fails */ public final Future deploy(Context context) throws Exception { @@ -209,14 +165,12 @@ public final Future deploy(Context context) throws Exception { } /** - * Internal helper method to simulate undeployment under Vert.x 5.x’s {@code Deployable} interface. + * Stop the deployable. *

- * User code should not call this directly. It is used by Vert.x internals or - * integration layers that work with Vert.x 5.x’s deployment model. - *

+ * Vert.x calls this method when undeploying this deployable. You do not call it yourself. * - * @param context the Vert.x context - * @return a future that completes when undeployment is finished + * @param context the Vert.x context assigned to this deployable + * @return a future signaling the clean-up completion * @throws Exception if undeployment fails */ public final Future undeploy(Context context) throws Exception { @@ -236,11 +190,10 @@ public final Future undeploy(Context context) throws Exception { * * @param action the handler to run */ - public void runOnContext(Handler action) { + protected void runOnContext(Handler action) { vertxContext.runOnContext(action); } - /** * Executes blocking code asynchronously, returning a {@link Future} that completes * when the blocking operation is done. @@ -249,7 +202,7 @@ public void runOnContext(Handler action) { * @param the result type * @return a future representing the blocking operation result */ - public Future executeBlocking(Callable blockingCodeHandler) { + protected Future executeBlocking(Callable blockingCodeHandler) { return vertxContext.executeBlocking(blockingCodeHandler); } @@ -262,7 +215,168 @@ public Future executeBlocking(Callable blockingCodeHandler) { * @param the result type * @return a future representing the blocking operation result */ - public Future executeBlocking(Callable blockingCodeHandler, boolean ordered) { + protected Future executeBlocking(Callable blockingCodeHandler, boolean ordered) { return vertxContext.executeBlocking(blockingCodeHandler, ordered); } -} \ No newline at end of file +} + +// BosonVerticle for Vert.x 4.5.x +// public abstract class BosonVerticle implements Verticle { +// /** +// * Reference to the Vert.x instance that deployed this verticle +// */ +// protected Vertx vertx; +// +// /** +// * Reference to the context of the verticle +// */ +// protected Context vertxContext; +// +// /** +// * Returns the Vert.x instance that deployed this Verticle. +// * +// * @return the Vert.x instance +// */ +// public final Vertx getVertx() { +// return vertx; +// } +// +// /** +// * Returns the Vert.x context associated with this Verticle. +// * +// * @return the Vert.x context +// */ +// protected final Context vertxContext() { +// return vertxContext; +// } +// +// /** +// * Returns the deployment ID of this Verticle deployment. +// * +// * @return the deployment ID +// */ +// public final String deploymentID() { +// return vertxContext.deploymentID(); +// } +// +// /** +// * Returns the configuration object of this Verticle deployment. +// *

+// * This configuration can be specified when the Verticle is deployed. +// *

+// * +// * @return the configuration as a {@link JsonObject} +// */ +// protected final JsonObject vertxConfig() { +// return vertxContext.config(); +// } +// +// /** +// * Prepares this verticle for deployment. +// *

+// * This method is invoked internally by {@link #init(Vertx, Context)} and can be overridden +// * if additional setup is needed before deployment. +// *

+// * +// * @param vertx the Vert.x instance +// * @param context the Vert.x context +// */ +// protected void prepare(Vertx vertx, Context context) { +// this.vertx = vertx; +// this.vertxContext = context; +// } +// +// /** +// * Called during startup to perform asynchronous initialization logic. +// *

+// * This method is invoked by {@link #start(Promise)}. +// *

+// * +// * @return a future that completes when setup is finished +// */ +// protected abstract Future deploy(); +// +// /** +// * Called during shutdown to perform asynchronous cleanup logic. +// *

+// * This method is invoked by {@link #stop(Promise)}. +// *

+// * +// * @return a future that completes when teardown is finished +// */ +// protected abstract Future undeploy(); +// +// /** +// * Initialise the verticle with the Vert.x instance and the context. +// *

+// * This method is called by Vert.x when the instance is deployed. You do not call it yourself. +// * +// * @param vertx the Vert.x instance +// * @param context the context +// */ +// public final void init(Vertx vertx, Context context) { +// prepare(vertx, context); +// } +// +// /** +// * Start the verticle instance. +// *

+// * Vert.x calls this method when deploying the instance. You do not call it yourself. +// *

+// * A promise is passed into the method, and when deployment is complete the verticle should either call +// * {@link io.vertx.core.Promise#complete} or {@link io.vertx.core.Promise#fail} the future. +// * +// * @param startPromise the future +// */ +// public final void start(Promise startPromise) throws Exception { +// deploy().onComplete(startPromise); +// } +// +// /** +// * Stop the verticle instance. +// *

+// * Vert.x calls this method when un-deploying the instance. You do not call it yourself. +// *

+// * A promise is passed into the method, and when un-deployment is complete the verticle should either call +// * {@link io.vertx.core.Promise#complete} or {@link io.vertx.core.Promise#fail} the future. +// * +// * @param stopPromise the future +// */ +// public final void stop(Promise stopPromise) throws Exception { +// undeploy().onComplete(stopPromise); +// } +// +// /** +// * Executes the given handler on this verticle's context. +// * +// * @param action the handler to run +// */ +// protected void runOnContext(Handler action) { +// vertxContext.runOnContext(action); +// } +// +// /** +// * Executes blocking code asynchronously, returning a {@link Future} that completes +// * when the blocking operation is done. +// * +// * @param blockingCodeHandler the blocking code to execute +// * @param the result type +// * @return a future representing the blocking operation result +// */ +// protected Future executeBlocking(Callable blockingCodeHandler) { +// return vertxContext.executeBlocking(blockingCodeHandler); +// } +// +// /** +// * Executes blocking code asynchronously, optionally ordering execution relative +// * to other blocking operations in the same context. +// * +// * @param blockingCodeHandler the blocking code to execute +// * @param ordered whether execution should be ordered +// * @param the result type +// * @return a future representing the blocking operation result +// */ +// protected Future executeBlocking(Callable blockingCodeHandler, boolean ordered) { +// return vertxContext.executeBlocking(blockingCodeHandler, ordered); +// } +//} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java b/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java new file mode 100644 index 0000000..fdffa32 --- /dev/null +++ b/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 - bosonnetwork.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bosonnetwork.vertx; + +import java.io.InputStream; + +import io.vertx.core.buffer.Buffer; + +/** + * A wrapper for Vert.x Buffer that implements InputStream + * to allow zero-copy reading by Jackson and other utilities. + */ +public class BufferInputStream extends InputStream { + private final Buffer buffer; + private int pos; + private final int limit; + private int markPos = 0; + + /** + * Constructs a new BufferInputStream instance to facilitate reading data from the specified + * Vert.x Buffer. The stream provides a zero-copy mechanism for efficient data handling. + * + * @param buffer the Vert.x Buffer from which data will be read + */ + public BufferInputStream(Buffer buffer) { + this.buffer = buffer; + this.pos = 0; + this.limit = buffer.length(); + } + + /** {@inheritDoc} */ + @Override + public int read() { + if (pos >= limit) + return -1; + + return buffer.getByte(pos++) & 0xFF; + } + + /** {@inheritDoc} */ + @Override + public int read(byte[] b, int off, int len) { + if (pos >= limit) + return -1; + + int available = limit - pos; + int toRead = Math.min(len, available); + buffer.getBytes(pos, pos + toRead, b, off); + pos += toRead; + return toRead; + } + + /** {@inheritDoc} */ + @Override + public int available() { + return limit - pos; + } + + /** {@inheritDoc} */ + @Override + public boolean markSupported() { + return true; + } + + /** {@inheritDoc} */ + @Override + public synchronized void mark(int readlimit) { + markPos = pos; + } + + /** {@inheritDoc} */ + @Override + public synchronized void reset() { + pos = markPos; + } +} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java b/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java new file mode 100644 index 0000000..4213856 --- /dev/null +++ b/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 - bosonnetwork.io + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.bosonnetwork.vertx; + +import java.io.OutputStream; + +import io.vertx.core.buffer.Buffer; + +/** + * A wrapper for Vert.x Buffer that implements OutputStream + * to allow zero-copy writing by Jackson and other utilities. + */ +public class BufferOutputStream extends OutputStream { + private final Buffer buffer; + + /** + * Constructs a new BufferOutputStream instance that wraps the provided Vert.x Buffer. + * This allows data to be written to the Buffer through a standard OutputStream interface. + * + * @param buffer the Vert.x Buffer to wrap and write data into + */ + public BufferOutputStream(Buffer buffer) { + this.buffer = buffer; + } + + /** {@inheritDoc} */ + @Override + public void write(int b) { + buffer.appendByte((byte) b); + } + + /** {@inheritDoc} */ + @Override + public void write(byte[] b, int off, int len) { + buffer.appendBytes(b, off, len); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/web/AccessToken.java b/api/src/main/java/io/bosonnetwork/web/AccessToken.java index 9d90f2d..69dd759 100644 --- a/api/src/main/java/io/bosonnetwork/web/AccessToken.java +++ b/api/src/main/java/io/bosonnetwork/web/AccessToken.java @@ -23,7 +23,6 @@ package io.bosonnetwork.web; import java.io.IOException; -import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -41,35 +40,58 @@ *

*/ public class AccessToken { - private static final Base64.Encoder B64encoder = Base64.getUrlEncoder().withoutPadding(); + private static final long MAX_TOKEN_LIFETIME = 30 * 60; // 30 minutes in seconds private final Identity issuer; + private final long defaultTTL; /** * Creates a new AccessToken generator for the given identity. - * + * * @param issuer the identity that will issue and sign the tokens + * @param defaultTTL the default time-to-live in seconds. If 0, the default value is used */ - public AccessToken(Identity issuer) { + public AccessToken(Identity issuer, long defaultTTL) { + if (defaultTTL < 0) + throw new IllegalArgumentException("defaultTTL must be positive"); + this.issuer = Objects.requireNonNull(issuer, "issuer cannot be null"); + this.defaultTTL = defaultTTL == 0 ? MAX_TOKEN_LIFETIME : defaultTTL; + } + + /** + * Creates a new AccessToken generator for the given identity with the default TTL. + * + * @param issuer the identity that will issue and sign the tokens + */ + public AccessToken(Identity issuer) { + this(issuer, 0); } /** * Generates a signed token. - * - * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device) - * @param associated the associated entity ID (optional, e.g. device ID) - * @param audience the audience ID (the server node ID) - * @param scope the scope string (optional) - * @param ttl the time-to-live in seconds + * + * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device) + * @param associated the associated entity ID (optional, e.g.; device ID) + * @param audience the audience ID (the server node ID) + * @param scope the scope string (optional) + * @param ttl the time-to-live in seconds * @return the generated token string */ - private String generate(Id subject, Id associated, Id audience, String scope, long ttl) { + protected String generate(Id subject, Id associated, Id audience, String scope, long ttl) { Objects.requireNonNull(subject, "subject cannot be null"); - Objects.requireNonNull(audience, "audience cannot be null"); + Objects.requireNonNull(audience, "audience cannot be null"); if (ttl <= 0) throw new IllegalArgumentException("ttl must be positive"); + if (associated != null) { + if (!associated.equals(issuer.getId())) + throw new IllegalArgumentException("associated must be the issuer ID"); + } else { + if (!subject.equals(issuer.getId())) + throw new IllegalArgumentException("subject must be the issuer ID"); + } + Map claims = new LinkedHashMap<>(); claims.put("jti", Random.randomBytes(24)); claims.put("iss", issuer.getId().bytes()); @@ -77,9 +99,9 @@ private String generate(Id subject, Id associated, Id audience, String scope, lo if (associated != null) claims.put("asc", associated.bytes()); claims.put("aud", audience.bytes()); - if (scope != null && !scope.isEmpty()) + if (scope != null && !scope.isEmpty()) claims.put("scp", scope); - + long now = System.currentTimeMillis() / 1000; claims.put("exp", now + ttl); @@ -92,31 +114,54 @@ private String generate(Id subject, Id associated, Id audience, String scope, lo byte[] sig = issuer.sign(payload); - return B64encoder.encodeToString(payload) + "." + B64encoder.encodeToString(sig); + return Json.BASE64_ENCODER.encodeToString(payload) + "." + Json.BASE64_ENCODER.encodeToString(sig); } /** * Generates a signed token as the subject and without associated entity. * * @param audience the audience ID (the server node ID) - * @param scope the scope string (optional) - * @param ttl the time-to-live in seconds + * @param scope the scope string (optional) + * @param ttl the time-to-live in seconds * @return the generated token string */ public String generate(Id audience, String scope, long ttl) { - return generate(issuer.getId(), null, audience, scope, ttl); + return generate(issuer.getId(), null, audience, scope, ttl == 0 ? defaultTTL : ttl); + } + + /** + * Generates a signed token as the subject and without associated entity. + * + * @param audience the audience ID (the server node ID) + * @param scope the scope string (optional) + * @return the generated token string + */ + public String generate(Id audience, String scope) { + return generate(audience, scope, 0); } /** * Generates a signed token as the associated entity. * - * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device) + * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device) * @param audience the audience ID (the server node ID) - * @param scope the scope string (optional) - * @param ttl the time-to-live in seconds + * @param scope the scope string (optional) + * @param ttl the time-to-live in seconds * @return the generated token string */ public String generate(Id subject, Id audience, String scope, long ttl) { - return generate(subject, issuer.getId(), audience, scope, ttl); + return generate(subject, issuer.getId(), audience, scope, ttl == 0 ? defaultTTL : ttl); + } + + /** + * Generates a signed token as the associated entity. + * + * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device) + * @param audience the audience ID (the server node ID) + * @param scope the scope string (optional) + * @return the generated token string + */ + public String generate(Id subject, Id audience, String scope) { + return generate(subject, audience, scope, 0); } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java index 3893dce..dd0b489 100644 --- a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java +++ b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java @@ -23,7 +23,6 @@ package io.bosonnetwork.web; import java.io.IOException; -import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -39,12 +38,12 @@ import io.bosonnetwork.Id; import io.bosonnetwork.Identity; import io.bosonnetwork.crypto.Random; +import io.bosonnetwork.json.Json; import io.bosonnetwork.service.ClientAuthenticator; import io.bosonnetwork.service.ClientDevice; import io.bosonnetwork.service.ClientUser; import io.bosonnetwork.service.FederatedNode; import io.bosonnetwork.service.ServiceInfo; -import io.bosonnetwork.json.Json; import io.bosonnetwork.utils.Pair; /** @@ -82,11 +81,6 @@ * */ public class CompactWebTokenAuth implements AuthenticationProvider { - /** Base64 URL encoder without padding. */ - protected static final Base64.Encoder B64encoder = Base64.getUrlEncoder().withoutPadding(); - /** Base64 URL decoder. */ - protected static final Base64.Decoder B64decoder = Base64.getUrlDecoder(); - private static final long MAX_SERVER_ISSUED_TOKEN_LIFETIME = 14 * 24 * 60 * 60; // 14 days in seconds private static final long MAX_CLIENT_ISSUED_TOKEN_LIFETIME = 30 * 60; // 30 minutes in seconds private static final int DEFAULT_LEEWAY = 5 * 60; // 5 minutes in seconds @@ -122,7 +116,7 @@ public interface UserRepository { * by leveraging the given authenticator for security purposes. * * @param authenticator the {@code ClientAuthenticator} instance used to validate client authentication - * @return a {@code UserRepository} implementation that utilizes the given {@code ClientAuthenticator} + * @return a {@code UserRepository} implementation that uses the given {@code ClientAuthenticator} */ static UserRepository fromClientAuthenticator(ClientAuthenticator authenticator) { return new AuthenticatorUserRepo(authenticator); @@ -272,7 +266,7 @@ private CompactWebTokenAuth(Identity identity, UserRepository userRepository, * Creates a new instance of CompactWebTokenAuth. * * @param identity the identity of the current server node (used for signing and verification) - * @param userRepository the repository to lookup token subjects and associated entities + * @param userRepository the repository to look up token subjects and associated entities * @param maxServerIssuedTokenLifetime maximum lifetime for tokens issued by this server (seconds) * @param maxClientIssuedTokenLifetime maximum lifetime for tokens issued by clients (seconds) * @param leeway allowed clock skew (seconds) @@ -333,8 +327,8 @@ public Future authenticate(Credentials credentials) { final JsonObject claims; try { - payload = B64decoder.decode(token.substring(0, index)); - sig = B64decoder.decode(token.substring(index + 1)); + payload = Json.BASE64_DECODER.decode(token.substring(0, index)); + sig = Json.BASE64_DECODER.decode(token.substring(index + 1)); claims = new JsonObject(Json.cborMapper().readValue(payload, Json.mapType())); } catch (IllegalArgumentException | IOException e) { return Future.failedFuture("Invalid authorization token: format error"); @@ -444,7 +438,7 @@ public Future authenticate(Credentials credentials) { return Future.failedFuture("Invalid authorization token: wrong issuer"); } */ - if (!issuer.equals(Objects.requireNonNullElse(associated, subject))) + if (!Objects.equals(issuer, subject) && !Objects.equals(issuer, associated)) return Future.failedFuture("Invalid authorization token: wrong issuer"); if (audience == null) @@ -463,9 +457,9 @@ public Future authenticate(Credentials credentials) { return Future.failedFuture("Invalid authorization token: life time too long"); } - // verify the signature - if (!issuer.toSignatureKey().verify(payload, sig)) + if (!issuer.toSignatureKey().verify(payload, sig)) { return Future.failedFuture("Invalid authorization token: signature verification failed"); + } final String scope = claims.containsKey("scp") ? claims.getString("scp") : null; @@ -536,6 +530,18 @@ public Future authenticate(Credentials credentials) { }); } + private String encodeToken(Map claims) { + final byte[] payload; + try { + payload = Json.cborMapper().writeValueAsBytes(claims); + } catch (IOException e) { + throw new RuntimeException("INTERNAL ERROR: JSON serialization"); + } + + final byte[] sig = identity.sign(payload); + return Json.BASE64_ENCODER.encodeToString(payload) + "." + Json.BASE64_ENCODER.encodeToString(sig); + } + /** * Generates a new token with specific claims. * @@ -544,30 +550,29 @@ public Future authenticate(Credentials credentials) { * @throws IllegalArgumentException if expiration is invalid */ public String generateToken(Map claims) { - Map _claims; + Map _claims = null; long now = System.currentTimeMillis() / 1000; if (claims.containsKey("exp") && claims.get("exp") != null) { long expiration = (Long) claims.get("exp"); if (expiration <= 0 || expiration > now + maxServerIssuedTokenLifetime) throw new IllegalArgumentException("Invalid expiration"); - - _claims = claims; } else { _claims = new LinkedHashMap<>(claims); _claims.put("exp", now + maxServerIssuedTokenLifetime); } - final byte[] payload; - try { - payload = Json.cborMapper().writeValueAsBytes(_claims); - } catch (IOException e) { - throw new RuntimeException("INTERNAL ERROR: JSON serialization"); + if (!claims.containsKey("jti")) { + if (_claims == null) + _claims = new LinkedHashMap<>(claims); + + _claims.put("jti", Random.randomBytes(24)); } - final byte[] sig = identity.sign(payload); + if (_claims == null) + _claims = claims; - return B64encoder.encodeToString(payload) + "." + B64encoder.encodeToString(sig); + return encodeToken(_claims); } /** @@ -576,17 +581,16 @@ public String generateToken(Map claims) { * @param subject the subject ID * @param associated the associated entity ID (optional, can be null) * @param scope the scope string (optional, can be null) - * @param expiration the expiration time in seconds (0 for default server lifetime) + * @param ttl the time-to-live in seconds (0 for default server lifetime) * @return the generated token string * @throws IllegalArgumentException if expiration is invalid */ - public String generateToken(Id subject, Id associated, String scope, long expiration) { + public String generateToken(Id subject, Id associated, String scope, long ttl) { Objects.requireNonNull(subject); - if (expiration < 0 || expiration > maxServerIssuedTokenLifetime) + if (ttl < 0 || ttl > maxServerIssuedTokenLifetime) throw new IllegalArgumentException("Invalid expiration"); - if (expiration == 0) - expiration = System.currentTimeMillis() / 1000 + maxServerIssuedTokenLifetime; + long expiration = System.currentTimeMillis() / 1000 + (ttl == 0 ? maxServerIssuedTokenLifetime : ttl); Map claims = new LinkedHashMap<>(5); claims.put("jti", Random.randomBytes(24)); @@ -596,7 +600,7 @@ public String generateToken(Id subject, Id associated, String scope, long expira if (scope != null && !scope.isEmpty()) claims.put("scp", scope); claims.put("exp", expiration); - return generateToken(claims); + return encodeToken(claims); } /** diff --git a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java index a43e352..c231f7e 100644 --- a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java +++ b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java @@ -106,7 +106,7 @@ public Future authenticate(RoutingContext context) { return authProvider.authenticate(credentials) .andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded())) - .recover(err -> Future.failedFuture(new HttpException(401, err))); + .recover(err -> Future.failedFuture(new HttpException(401, err.getMessage()))); }); } diff --git a/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java b/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java index 9ca7c55..37462ae 100644 --- a/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java +++ b/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java @@ -2,8 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; import io.vertx.ext.auth.authentication.TokenCredentials; import org.junit.jupiter.api.Assertions; @@ -15,10 +19,12 @@ import io.bosonnetwork.Id; import io.bosonnetwork.Identity; import io.bosonnetwork.crypto.CryptoIdentity; +import io.bosonnetwork.json.Json; import io.bosonnetwork.service.ClientDevice; import io.bosonnetwork.service.ClientUser; @ExtendWith(VertxExtension.class) +@SuppressWarnings("CodeBlock2Expr") public class AccessTokenTest { private static final Identity superNodeIdentity = new CryptoIdentity(); private static final Identity aliceIdentity = new CryptoIdentity(); @@ -43,13 +49,21 @@ public Future getAssociated(Id subject, Id associated) { private static final CompactWebTokenAuth auth = CompactWebTokenAuth.create(superNodeIdentity, repo, 3600, 3600, 0); + private static void printToken(String token) { + System.out.println("Token: " + token); + String[] parts = token.split("\\."); + String payload = Json.toString(Json.parse(Json.BASE64_DECODER.decode(parts[0]))); + System.out.println(" - " + payload); + System.out.println(" - " + parts[1]); + } + @Test void testGenerateUserToken(VertxTestContext context) { AccessToken accessToken = new AccessToken(aliceIdentity); Id audience = superNodeIdentity.getId(); - String token = accessToken.generate(audience, "read write", 60); - System.out.println("Generated Token: " + token); + String token = accessToken.generate(audience, "read write"); + printToken(token); // Verify using CompactWebTokenAuth auth.authenticate(new TokenCredentials(token)).onComplete(context.succeeding(user -> { @@ -69,7 +83,8 @@ void testGenerateDeviceToken(VertxTestContext context) { AccessToken accessToken = new AccessToken(iPadIdentity); Id audience = superNodeIdentity.getId(); - String token = accessToken.generate(alice.getId(), audience, "read write", 60); + String token = accessToken.generate(alice.getId(), audience, "read write"); + printToken(token); auth.authenticate(new TokenCredentials(token)).onComplete(context.succeeding(user -> { context.verify(() -> { @@ -82,4 +97,114 @@ void testGenerateDeviceToken(VertxTestContext context) { }); })); } + + @Test + void testGenerateUserTokenWithWrongAudience(VertxTestContext context) { + AccessToken accessToken = new AccessToken(aliceIdentity); + Id audience = Id.random(); + + String token = accessToken.generate(audience, "read write"); + printToken(token); + + // Verify using CompactWebTokenAuth + auth.authenticate(new TokenCredentials(token)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("wrong audience")); + context.completeNow(); + }); + }); + } + + @Test + void testGenerateDeviceTokenWithWrongAudience(VertxTestContext context) { + AccessToken accessToken = new AccessToken(iPadIdentity); + Id audience = Id.random(); + + String token = accessToken.generate(alice.getId(), audience, "read write"); + printToken(token); + + auth.authenticate(new TokenCredentials(token)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("wrong audience")); + context.completeNow(); + }); + }); + } + + @Test + void testExpiredUserAndDeviceToken(Vertx vertx, VertxTestContext context) { + Id audience = superNodeIdentity.getId(); + + String userToken = new AccessToken(aliceIdentity, 10).generate(audience, "read write"); + printToken(userToken); + + String deviceToken = new AccessToken(iPadIdentity, 10).generate(alice.getId(), audience, "read write"); + printToken(deviceToken); + + Promise promise = Promise.promise(); + vertx.setTimer(10000, l -> promise.complete()); + System.out.println("Waiting for token expiration..."); + + promise.future().compose(v -> { + Future f1 = auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("expired")); + }); + }).mapEmpty(); + + Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("expired")); + }); + }).mapEmpty(); + + return Future.join(f1, f2).otherwiseEmpty(); + }).onComplete(context.succeedingThenComplete()); + } + + @Test + void testGenerateUserTokenWithWrongSubject() { + AccessToken accessToken = new AccessToken(aliceIdentity); + Id audience = superNodeIdentity.getId(); + + Exception e = assertThrows(IllegalArgumentException.class, () -> { + accessToken.generate(Id.random(), null, audience, "read write", 60); + }); + assertTrue(e.getMessage().contains("subject must be the issuer ID")); + } + + @Test + void testGenerateDeviceTokenWithWrongSubject() { + AccessToken accessToken = new AccessToken(iPadIdentity); + Id audience = superNodeIdentity.getId(); + + Exception e = assertThrows(IllegalArgumentException.class, () -> { + accessToken.generate(alice.getId(), Id.random(), audience, "read write", 60); + }); + assertTrue(e.getMessage().contains("associated must be the issuer ID")); + } + + @Test + void testGenerateUserTokenWithoutAudience() { + AccessToken accessToken = new AccessToken(aliceIdentity); + + Exception e = assertThrows(NullPointerException.class, () -> { + accessToken.generate(null, "read write"); + }); + assertTrue(e.getMessage().contains("audience cannot be null")); + } + + @Test + void testGenerateDeviceTokenWithoutAudience() { + AccessToken accessToken = new AccessToken(iPadIdentity); + + Exception e = assertThrows(NullPointerException.class, () -> { + accessToken.generate(alice.getId(), iPad.getId(), null, "read write", 60); + }); + assertTrue(e.getMessage().contains("audience cannot be null")); + } } \ No newline at end of file diff --git a/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java b/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java index dbf5c30..0bef113 100644 --- a/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java +++ b/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java @@ -31,8 +31,9 @@ import io.bosonnetwork.json.Json; @ExtendWith(VertxExtension.class) +@SuppressWarnings("CodeBlock2Expr") public class CompactWebTokenAuthTest { - private static final long DEFAULT_LIFETIME = 10; + private static final long DEFAULT_LIFETIME = 10; // 10 seconds private static final Identity superNodeIdentity = new CryptoIdentity(); private static final Signature.KeyPair aliceKeyPair = Signature.KeyPair.random(); private static final Signature.KeyPair iPadKeyPair = Signature.KeyPair.random(); @@ -58,10 +59,18 @@ public Future getAssociated(Id subject, Id associated) { private static final CompactWebTokenAuth auth = CompactWebTokenAuth.create(superNodeIdentity, repo, DEFAULT_LIFETIME, DEFAULT_LIFETIME, 0); + private static void printToken(String token) { + System.out.println("Token: " + token); + String[] parts = token.split("\\."); + String payload = Json.toString(Json.parse(Json.BASE64_DECODER.decode(parts[0]))); + System.out.println(" - " + payload); + System.out.println(" - " + parts[1]); + } + @Test void testSuperNodeIssuedToken(VertxTestContext context) { String userToken = auth.generateToken(alice.getId(), "test"); - System.out.println(userToken); + printToken(userToken); Future f1 = auth.authenticate(new TokenCredentials(userToken)).andThen(context.succeeding(user -> { context.verify(() -> { assertNotNull(user); @@ -78,7 +87,7 @@ void testSuperNodeIssuedToken(VertxTestContext context) { })); String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test"); - System.out.println(deviceToken); + printToken(deviceToken); Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).andThen(context.succeeding(user -> { context.verify(() -> { assertNotNull(user); @@ -99,42 +108,42 @@ void testSuperNodeIssuedToken(VertxTestContext context) { @Test void testSuperNodeIssuedAndExpiredToken(Vertx vertx, VertxTestContext context) { String userToken = auth.generateToken(alice.getId(), "test"); - System.out.println(userToken); + printToken(userToken); String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test"); - System.out.println(deviceToken); + printToken(deviceToken); Promise promise = Promise.promise(); vertx.setTimer(10000, l -> promise.complete()); System.out.println("Waiting for token expiration..."); promise.future().compose(v -> { - Future f1 = auth.authenticate(new TokenCredentials(userToken)).andThen(ar -> { + Future f1 = auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> { context.verify(() -> { assertTrue(ar.failed()); assertTrue(ar.cause().getMessage().contains("expired")); }); - }).otherwiseEmpty(); + }); - Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).andThen(ar -> { + Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> { context.verify(() -> { assertTrue(ar.failed()); assertTrue(ar.cause().getMessage().contains("expired")); }); - }).otherwiseEmpty(); + }); - return Future.all(f1, f2); + return Future.all(f1, f2).otherwiseEmpty(); }).andThen(context.succeedingThenComplete()); } @Test - void testSuperNodeIssuedAndInvalidSigToken(Vertx vertx, VertxTestContext context) { + void testSuperNodeIssuedAndInvalidSigToken(VertxTestContext context) { String userToken = auth.generateToken(alice.getId(), "test"); - System.out.println(userToken); + printToken(userToken); byte[] sig = Json.BASE64_DECODER.decode(userToken.substring(userToken.lastIndexOf('.') + 1)); sig[0] = (byte) ~sig[0]; String invalidUserToken = userToken.substring(0, userToken.lastIndexOf('.')) + '.' + Json.BASE64_ENCODER.encodeToString(sig); String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test"); - System.out.println(deviceToken); + printToken(deviceToken); sig = Json.BASE64_DECODER.decode(deviceToken.substring(deviceToken.lastIndexOf('.') + 1)); sig[0] = (byte) ~sig[0]; String invalidDeviceToken = deviceToken.substring(0, deviceToken.lastIndexOf('.')) + '.' + Json.BASE64_ENCODER.encodeToString(sig); @@ -198,7 +207,7 @@ private String generateClientToken(Signature.KeyPair signer, Id subject, Id asso @Test void testClientIssuedToken(VertxTestContext context) throws Exception { String userToken = generateClientToken(aliceKeyPair, alice.getId(), null, superNodeIdentity.getId(), 0, "test"); - System.out.println("ClientToken: " + userToken); + printToken(userToken); auth.authenticate(new TokenCredentials(userToken)).onComplete(context.succeeding(user -> { context.verify(() -> { @@ -216,7 +225,7 @@ void testClientIssuedToken(VertxTestContext context) throws Exception { @Test void testDeviceIssuedToken(VertxTestContext context) throws Exception { String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), superNodeIdentity.getId(), 0, "test"); - System.out.println("DeviceToken: " + deviceToken); + printToken(deviceToken); auth.authenticate(new TokenCredentials(deviceToken)).onComplete(context.succeeding(user -> { context.verify(() -> { @@ -242,5 +251,63 @@ void testClientIssuedTokenWrongAudience(VertxTestContext context) throws Excepti context.completeNow(); }); }); + + // Wrong audience (random ID) + String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), Id.random(), 0, "test"); + + auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("wrong audience")); + context.completeNow(); + }); + }); + } + + @Test + void testClientIssuedTokenWithoutAudience(VertxTestContext context) throws Exception { + String userToken = generateClientToken(aliceKeyPair, alice.getId(), null, null, 0, "test"); + + auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("missing audience")); + context.completeNow(); + }); + }); + + String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), null, 0, "test"); + + auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("missing audience")); + context.completeNow(); + }); + }); + } + + @Test + void testClientIssuedTokenWrongIssuer(VertxTestContext context) throws Exception { + // Wrong issuer + String userToken = generateClientToken(aliceKeyPair, Id.random(), null, Id.random(), 0, "test"); + + auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("wrong issuer")); + context.completeNow(); + }); + }); + + String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), Id.random(), Id.random(), 0, "test"); + + auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> { + context.verify(() -> { + assertTrue(ar.failed()); + assertTrue(ar.cause().getMessage().contains("wrong issuer")); + context.completeNow(); + }); + }); } } \ No newline at end of file diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java index 610a4ed..365393d 100644 --- a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java +++ b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java @@ -221,14 +221,14 @@ public VertxFuture stop() { } @Override - public void prepare(Vertx vertx, Context context) { + protected void prepare(Vertx vertx, Context context) { super.prepare(vertx, context); identity.initCache(VertxCaffeine.newBuilder(vertx) .expireAfterAccess(KBucketEntry.OLD_AND_STALE_TIME, TimeUnit.MILLISECONDS)); } @Override - public Future deploy() { + protected Future deploy() { tokenManager = new TokenManager(); String storageURI = config.databaseUri(); @@ -326,7 +326,7 @@ public void disconnected(Network network) { } @Override - public Future undeploy() { + protected Future undeploy() { running = false; return Future.succeededFuture().andThen(ar -> { diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java b/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java index 91f042a..cdc47fb 100644 --- a/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java +++ b/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java @@ -201,13 +201,13 @@ public void setConnectionStatusListener(ConnectionStatusListener listener) { } @Override - public void prepare(Vertx vertx, Context context) { + protected void prepare(Vertx vertx, Context context) { super.prepare(vertx, context); this.kadContext = new KadContext(vertx, context, identity, network, this, enableDeveloperMode); } @Override - public Future deploy() { + protected Future deploy() { if (running) return Future.succeededFuture(); @@ -279,7 +279,7 @@ public Future deploy() { } @Override - public Future undeploy() { + protected Future undeploy() { if (!running) return Future.succeededFuture(); diff --git a/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java b/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java index bc597c3..0b97ebe 100644 --- a/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java +++ b/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java @@ -123,7 +123,7 @@ public Network getNetwork() { } @Override - public void prepare(Vertx vertx, Context context) { + protected void prepare(Vertx vertx, Context context) { super.prepare(vertx, context); kadContext = new KadContext(vertx, context, identity, getNetwork(), null); @@ -133,12 +133,12 @@ public void prepare(Vertx vertx, Context context) { } @Override - public Future deploy() { + protected Future deploy() { return rpcServer.start(); } @Override - public Future undeploy() { + protected Future undeploy() { if (rpcServer != null) return rpcServer.stop().andThen(ar -> rpcServer = null); else