Repository: eclipse/leshan
Pull request #1028 (2021-06-17 09:42:53)

Add Auto ID for Bootstrap Server Security Object(0) instance
LWM2M does not allow to delete the instance of Security Object(0) related to Bootstrap server.

This could be an issue to apply a bootstrap config if you don't know what is the instance ID of BS server before.

The auto ID mode uses Bootstrap Discover Request to solve this issue.

See for more details : https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/522

Some additional feature was added too : 
- "Bootstrap-Request Trigger" resource (1/?/9) at client side.
- support of "uri" attribute on bootstrap discover
- #1000: Add a BootstrapConsistencyChecker to Leshan Client. 


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
index 3682a5fd..1b7ccce2 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
@@ -33,7 +33,9 @@ import org.eclipse.californium.elements.util.NamedThreadFactory;
 import org.eclipse.californium.scandium.config.DtlsConnectorConfig.Builder;
 import org.eclipse.leshan.client.LwM2mClient;
 import org.eclipse.leshan.client.RegistrationUpdateHandler;
+import org.eclipse.leshan.client.bootstrap.BootstrapConsistencyChecker;
 import org.eclipse.leshan.client.bootstrap.BootstrapHandler;
+import org.eclipse.leshan.client.bootstrap.DefaultBootstrapConsistencyChecker;
 import org.eclipse.leshan.client.californium.bootstrap.BootstrapResource;
 import org.eclipse.leshan.client.californium.object.ObjectResource;
 import org.eclipse.leshan.client.californium.request.CaliforniumLwM2mRequestSender;
@@ -104,15 +106,17 @@ public class LeshanClient implements LwM2mClient {
             Map<String, String> additionalAttributes, Map<String, String> bsAdditionalAttributes,
             LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) {
         this(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, null, endpointFactory,
-                engineFactory, additionalAttributes, bsAdditionalAttributes, encoder, decoder, sharedExecutor);
+                engineFactory, new DefaultBootstrapConsistencyChecker(), additionalAttributes, bsAdditionalAttributes,
+                encoder, decoder, sharedExecutor);
     }
 
     /** @since 2.0 */
     public LeshanClient(String endpoint, InetSocketAddress localAddress,
             List<? extends LwM2mObjectEnabler> objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder,
             List<Certificate> trustStore, EndpointFactory endpointFactory, RegistrationEngineFactory engineFactory,
-            Map<String, String> additionalAttributes, Map<String, String> bsAdditionalAttributes,
-            LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder, ScheduledExecutorService sharedExecutor) {
+            BootstrapConsistencyChecker checker, Map<String, String> additionalAttributes,
+            Map<String, String> bsAdditionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
+            ScheduledExecutorService sharedExecutor) {
 
         Validate.notNull(endpoint);
         Validate.notEmpty(objectEnablers);
@@ -123,7 +127,7 @@ public class LeshanClient implements LwM2mClient {
         this.decoder = decoder;
         this.encoder = encoder;
         observers = createClientObserverDispatcher();
-        bootstrapHandler = createBoostrapHandler(objectTree);
+        bootstrapHandler = createBoostrapHandler(objectTree, checker);
         endpointsManager = createEndpointsManager(localAddress, coapConfig, dtlsConfigBuilder, trustStore,
                 endpointFactory);
         requestSender = createRequestSender(endpointsManager, sharedExecutor, encoder, objectTree.getModel());
@@ -160,8 +164,8 @@ public class LeshanClient implements LwM2mClient {
         return observer;
     }
 
-    protected BootstrapHandler createBoostrapHandler(LwM2mObjectTree objectTree) {
-        return new BootstrapHandler(objectTree.getObjectEnablers());
+    protected BootstrapHandler createBoostrapHandler(LwM2mObjectTree objectTree, BootstrapConsistencyChecker checker) {
+        return new BootstrapHandler(objectTree.getObjectEnablers(), checker);
     }
 
     protected CoapServer createCoapServer(NetworkConfig coapConfig, ScheduledExecutorService sharedExecutor) {
@@ -304,6 +308,11 @@ public class LeshanClient implements LwM2mClient {
         engine.triggerRegistrationUpdate(server);
     }
 
+    @Override
+    public boolean triggerClientInitiatedBootstrap(boolean deregister) {
+        return engine.triggerClientInitiatedBootstrap(deregister);
+    }
+
     @Override
     public SendResponse sendData(ServerIdentity server, ContentFormat format, List<String> paths, long timeoutInMs)
             throws InterruptedException {
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
index be06ab02..aea71ed0 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClientBuilder.java
@@ -31,6 +31,8 @@ import org.eclipse.californium.elements.UDPConnector;
 import org.eclipse.californium.scandium.DTLSConnector;
 import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
 import org.eclipse.californium.scandium.config.DtlsConnectorConfig.Builder;
+import org.eclipse.leshan.client.bootstrap.BootstrapConsistencyChecker;
+import org.eclipse.leshan.client.bootstrap.DefaultBootstrapConsistencyChecker;
 import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
 import org.eclipse.leshan.client.engine.RegistrationEngine;
 import org.eclipse.leshan.client.engine.RegistrationEngineFactory;
@@ -71,11 +73,11 @@ public class LeshanClientBuilder {
     private EndpointFactory endpointFactory;
     private RegistrationEngineFactory engineFactory;
     private Map<String, String> additionalAttributes;
+    private Map<String, String> bsAdditionalAttributes;
 
-    private ScheduledExecutorService executor;
+    private BootstrapConsistencyChecker bootstrapConsistencyChecker;
 
-    /** @since 1.1 */
-    protected Map<String, String> bsAdditionalAttributes;
+    private ScheduledExecutorService executor;
 
     /**
      * Creates a new instance for setting the configuration options for a {@link LeshanClient} instance.
@@ -217,6 +219,18 @@ public class LeshanClientBuilder {
         return this;
     }
 
+    /**
+     * Set a {@link BootstrapConsistencyChecker} which is used to valid client state after a bootstrap session.
+     * <p>
+     * By default a {@link DefaultBootstrapConsistencyChecker} is used.
+     * 
+     * @return the builder for fluent client creation.
+     */
+    public LeshanClientBuilder setBootstrapConsistencyChecker(BootstrapConsistencyChecker checker) {
+        this.bootstrapConsistencyChecker = checker;
+        return this;
+    }
+
     /**
      * Set a shared executor. This executor will be used everywhere it is possible. This is generally used when you want
      * to limit the number of thread to use or if you want to simulate a lot of clients sharing the same thread pool.
@@ -283,6 +297,9 @@ public class LeshanClientBuilder {
                 }
             };
         }
+        if (bootstrapConsistencyChecker == null) {
+            bootstrapConsistencyChecker = new DefaultBootstrapConsistencyChecker();
+        }
 
         // handle dtlsConfig
         if (dtlsConfigBuilder == null) {
@@ -324,7 +341,8 @@ public class LeshanClientBuilder {
         }
 
         return createLeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder,
-                this.trustStore, endpointFactory, engineFactory, additionalAttributes, encoder, decoder, executor);
+                this.trustStore, endpointFactory, engineFactory, bootstrapConsistencyChecker, additionalAttributes,
+                bsAdditionalAttributes, encoder, decoder, executor);
     }
 
     /**
@@ -344,7 +362,9 @@ public class LeshanClientBuilder {
      * @param trustStore The optional trust store for verifying X.509 server certificates.
      * @param endpointFactory The factory which will create the {@link CoapEndpoint}.
      * @param engineFactory The factory which will create the {@link RegistrationEngine}.
+     * @param checker Used to check if client state is consistent after a bootstrap session.
      * @param additionalAttributes Some extra (out-of-spec) attributes to add to the register request.
+     * @param bsAdditionalAttributes Some extra (out-of-spec) attributes to add to the bootstrap request.
      * @param encoder used to encode request payload.
      * @param decoder used to decode response payload.
      * @param sharedExecutor an optional shared executor.
@@ -354,10 +374,11 @@ public class LeshanClientBuilder {
     protected LeshanClient createLeshanClient(String endpoint, InetSocketAddress localAddress,
             List<? extends LwM2mObjectEnabler> objectEnablers, NetworkConfig coapConfig, Builder dtlsConfigBuilder,
             List<Certificate> trustStore, EndpointFactory endpointFactory, RegistrationEngineFactory engineFactory,
-            Map<String, String> additionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
+            BootstrapConsistencyChecker checker, Map<String, String> additionalAttributes,
+            Map<String, String> bsAdditionalAttributes, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder,
             ScheduledExecutorService sharedExecutor) {
         return new LeshanClient(endpoint, localAddress, objectEnablers, coapConfig, dtlsConfigBuilder, trustStore,
-                endpointFactory, engineFactory, additionalAttributes, bsAdditionalAttributes, encoder, decoder,
-                executor);
+                endpointFactory, engineFactory, checker, additionalAttributes, bsAdditionalAttributes, encoder, decoder,
+                sharedExecutor);
     }
 }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
index 8f2f3442..2c782e39 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/LwM2mClient.java
@@ -67,6 +67,15 @@ public interface LwM2mClient {
      */
     void triggerRegistrationUpdate(ServerIdentity server);
 
+    /**
+     * Trigger a client initiated bootstrap.
+     * 
+     * @param deregister True if client should deregister itself before to stop.
+     * 
+     * @return true if the bootstrap can be initiated.
+     */
+    boolean triggerClientInitiatedBootstrap(boolean deregister);
+
     /**
      * Send Data synchronously to a LWM2M Server.
      * <p>
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java
new file mode 100644
index 00000000..6957c4e7
--- /dev/null
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapConsistencyChecker.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.client.bootstrap;
+
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
+
+/**
+ * This class is responsible to check the consistency state of the client after at the end of a bootstrap session.
+ */
+public interface BootstrapConsistencyChecker {
+
+    /**
+     * check if current state of the client is consistent
+     * 
+     * @param objectEnablers all objects supported by the client.
+     * @return <code>null</code> if the current state is consistent or a list of issues
+     */
+    List<String> checkconfig(Map<Integer, LwM2mObjectEnabler> objectEnablers);
+
+}
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java
index 80ced315..bdfa846e 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/BootstrapHandler.java
@@ -16,6 +16,7 @@
  *******************************************************************************/
 package org.eclipse.leshan.client.bootstrap;
 
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -40,11 +41,15 @@ public class BootstrapHandler {
 
     private boolean bootstrapping = false;
     private CountDownLatch bootstrappingLatch = new CountDownLatch(1);
+    // last session state (null means no error)
+    private volatile List<String> lastConsistencyError = null;
 
     private final Map<Integer, LwM2mObjectEnabler> objects;
+    private BootstrapConsistencyChecker checker;
 
-    public BootstrapHandler(Map<Integer, LwM2mObjectEnabler> objectEnablers) {
+    public BootstrapHandler(Map<Integer, LwM2mObjectEnabler> objectEnablers, BootstrapConsistencyChecker checker) {
         objects = objectEnablers;
+        this.checker = checker;
     }
 
     public synchronized SendableResponse<BootstrapFinishResponse> finished(ServerIdentity server,
@@ -54,7 +59,6 @@ public class BootstrapHandler {
             if (!server.isLwm2mBootstrapServer()) {
                 return new SendableResponse<>(BootstrapFinishResponse.badRequest("not from a bootstrap server"));
             }
-            // TODO delete bootstrap server (see 5.2.5.2 Bootstrap Delete)
 
             Runnable whenSent = new Runnable() {
                 @Override
@@ -63,7 +67,17 @@ public class BootstrapHandler {
                 }
             };
 
-            return new SendableResponse<>(BootstrapFinishResponse.success(), whenSent);
+            // check consistency state of the client
+            lastConsistencyError = checker.checkconfig(objects);
+            if (lastConsistencyError == null) {
+                return new SendableResponse<>(BootstrapFinishResponse.success(), whenSent);
+            } else {
+                // TODO rollback configuration.
+                // see https://github.com/eclipse/leshan/issues/968
+                return new SendableResponse<>(BootstrapFinishResponse.notAcceptable(lastConsistencyError.toString()),
+                        whenSent);
+            }
+
         } else {
             return new SendableResponse<>(BootstrapFinishResponse.badRequest("no pending bootstrap session"));
         }
@@ -91,6 +105,7 @@ public class BootstrapHandler {
         if (!bootstrapping) {
             bootstrappingLatch = new CountDownLatch(1);
             bootstrapping = true;
+            lastConsistencyError = null;
             return true;
         }
         return false;
@@ -100,8 +115,15 @@ public class BootstrapHandler {
         return bootstrapping;
     }
 
-    public boolean waitBoostrapFinished(long timeInSeconds) throws InterruptedException {
-        return bootstrappingLatch.await(timeInSeconds, TimeUnit.SECONDS);
+    public boolean waitBoostrapFinished(long timeInSeconds) throws InterruptedException, InvalidStateException {
+        boolean finished = bootstrappingLatch.await(timeInSeconds, TimeUnit.SECONDS);
+        if (finished) {
+            if (lastConsistencyError != null) {
+                throw new InvalidStateException(
+                        String.format("Invalid Bootstrap state : %s", lastConsistencyError.toString()));
+            }
+        }
+        return finished;
     }
 
     public synchronized void closeSession() {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java
new file mode 100644
index 00000000..d1546b0f
--- /dev/null
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/DefaultBootstrapConsistencyChecker.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.client.bootstrap;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
+import org.eclipse.leshan.client.servers.ServersInfoExtractor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A default implementation of {@link BootstrapConsistencyChecker}
+ */
+public class DefaultBootstrapConsistencyChecker implements BootstrapConsistencyChecker {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultBootstrapConsistencyChecker.class);
+
+    @Override
+    public List<String> checkconfig(Map<Integer, LwM2mObjectEnabler> objectEnablers) {
+        try {
+            ServersInfoExtractor.getInfo(objectEnablers, true);
+        } catch (RuntimeException e) {
+            LOG.debug(e.getMessage());
+            return Arrays.asList(e.getMessage());
+        }
+        return null;
+    }
+
+}
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java
new file mode 100644
index 00000000..dc64802e
--- /dev/null
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/bootstrap/InvalidStateException.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.client.bootstrap;
+
+/**
+ * Raised if LWM2M client is in an invalid or inconsistent state.
+ * 
+ */
+public class InvalidStateException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public InvalidStateException() {
+    }
+
+    public InvalidStateException(String m) {
+        super(m);
+    }
+
+    public InvalidStateException(String m, Object... args) {
+        super(String.format(m, args));
+    }
+
+    public InvalidStateException(Throwable e) {
+        super(e);
+    }
+
+    public InvalidStateException(String m, Throwable e) {
+        super(m, e);
+    }
+
+    public InvalidStateException(Throwable e, String m, Object... args) {
+        super(String.format(m, args), e);
+    }
+}
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
index f5e757a8..4b52e2b4 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/DefaultRegistrationEngine.java
@@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicReference;
 import org.eclipse.leshan.client.EndpointsManager;
 import org.eclipse.leshan.client.RegistrationUpdate;
 import org.eclipse.leshan.client.bootstrap.BootstrapHandler;
+import org.eclipse.leshan.client.bootstrap.InvalidStateException;
 import org.eclipse.leshan.client.observer.LwM2mClientObserver;
 import org.eclipse.leshan.client.request.LwM2mRequestSender;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
@@ -233,25 +234,33 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
                 } else if (response.isSuccess()) {
                     LOG.info("Bootstrap started");
                     // Wait until it is finished (or too late)
-                    boolean timeout = !bootstrapHandler.waitBoostrapFinished(bootstrapSessionTimeoutInSec);
-                    if (timeout) {
-                        LOG.info("Bootstrap sequence aborted: Timeout.");
-                        if (observer != null) {
-                            observer.onBootstrapTimeout(bootstrapServer, request);
-                        }
-                        return null;
-                    } else {
-                        LOG.info("Bootstrap finished {}.", bootstrapServer.getUri());
-                        ServerInfo serverInfo = selectServer(
-                                ServersInfoExtractor.getInfo(objectEnablers).deviceManagements);
-                        ServerIdentity dmServer = null;
-                        if (serverInfo != null) {
-                            dmServer = endpointsManager.createEndpoint(serverInfo);
+                    try {
+                        boolean timeout = !bootstrapHandler.waitBoostrapFinished(bootstrapSessionTimeoutInSec);
+                        if (timeout) {
+                            LOG.info("Bootstrap sequence aborted: Timeout.");
+                            if (observer != null) {
+                                observer.onBootstrapTimeout(bootstrapServer, request);
+                            }
+                            return null;
+                        } else {
+                            LOG.info("Bootstrap finished {}.", bootstrapServer.getUri());
+                            ServerInfo serverInfo = selectServer(
+                                    ServersInfoExtractor.getInfo(objectEnablers).deviceManagements);
+                            ServerIdentity dmServer = null;
+                            if (serverInfo != null) {
+                                dmServer = endpointsManager.createEndpoint(serverInfo);
+                            }
+                            if (observer != null) {
+                                observer.onBootstrapSuccess(bootstrapServer, request);
+                            }
+                            return dmServer;
                         }
+                    } catch (InvalidStateException e) {
+                        LOG.info("Bootstrap finished with failure because of consistency check failure.", e);
                         if (observer != null) {
-                            observer.onBootstrapSuccess(bootstrapServer, request);
+                            observer.onBootstrapFailure(bootstrapServer, request, null, null, e);
                         }
-                        return dmServer;
+                        return null;
                     }
                 } else {
                     LOG.info("Bootstrap failed: {} {}.", response.getCode(), response.getErrorMessage());
@@ -760,6 +769,49 @@ public class DefaultRegistrationEngine implements RegistrationEngine {
         }
     }
 
+    @Override
+    public boolean triggerClientInitiatedBootstrap(final boolean deregister) {
+        synchronized (this) {
+            if (started) {
+
+                // check if we have a bootstrap server
+                ServerInfo bootstrapServerInfo = ServersInfoExtractor.getBootstrapServerInfo(objectEnablers);
+                if (bootstrapServerInfo == null) {
+                    return false;
+                }
+
+                // stop every
+                cancelUpdateTask(true);
+                cancelRegistrationTask();
+                // TODO we should manage the case where we stop in the middle of a bootstrap session ...
+                cancelBootstrapTask();
+
+                schedExecutor.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        try {
+                            // deregister if needed
+                            if (deregister) {
+                                if (!registeredServers.isEmpty()) {
+                                    for (Entry<String, ServerIdentity> registeredServer : registeredServers
+                                            .entrySet()) {
+                                        deregister(registeredServer.getValue(), registeredServer.getKey());
+                                    }
+                                }
+                            }
+                        } catch (InterruptedException e) {
+                        }
+
+                        // schedule a new bootstrap.
+                        scheduleClientInitiatedBootstrap(NOW);
+                    }
+                });
+            }
+        }
+        return true;
+    }
+
     private void logExceptionOnSendRequest(String message, Exception e) {
         if (LOG.isDebugEnabled()) {
             LOG.warn(message, e);
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java
index 98409ed9..8e790bfc 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/engine/RegistrationEngine.java
@@ -52,6 +52,15 @@ public interface RegistrationEngine {
      */
     void triggerRegistrationUpdate(ServerIdentity server, RegistrationUpdate registrationUpdate);
 
+    /**
+     * Trigger a client initiated bootstrap.
+     * 
+     * @param deregister True if client should deregister itself before to stop.
+     * 
+     * @return true if the bootstrap can be initiated.
+     */
+    boolean triggerClientInitiatedBootstrap(boolean deregister);
+
     /**
      * Returns the current registration Id for this server.
      * 
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java
index ee9a4088..9924e6fa 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/object/Server.java
@@ -201,6 +201,13 @@ public class Server extends BaseInstanceEnabler {
         if (resourceid == 8) {
             getLwM2mClient().triggerRegistrationUpdate(identity);
             return ExecuteResponse.success();
+        } else if (resourceid == 9) {
+            boolean success = getLwM2mClient().triggerClientInitiatedBootstrap(true);
+            if (success)
+                return ExecuteResponse.success();
+            else {
+                return ExecuteResponse.badRequest("probably no bootstrap server configured");
+            }
         } else {
             return super.execute(identity, resourceid, params);
         }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
index 78a444cd..69ed4e2b 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
@@ -410,6 +410,7 @@ public class ObjectEnabler extends BaseObjectEnabler implements Destroyable, Sta
 
     @Override
     public void setLwM2mClient(LwM2mClient client) {
+        super.setLwM2mClient(client);
         for (LwM2mInstanceEnabler instanceEnabler : instances.values()) {
             instanceEnabler.setLwM2mClient(client);
         }
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java
index 7e38f731..3a455cec 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/servers/ServersInfoExtractor.java
@@ -59,6 +59,12 @@ public class ServersInfoExtractor {
     private static final Logger LOG = LoggerFactory.getLogger(ServersInfoExtractor.class);
 
     public static ServersInfo getInfo(Map<Integer, LwM2mObjectEnabler> objectEnablers) {
+        return getInfo(objectEnablers, false);
+    }
+
+    public static ServersInfo getInfo(Map<Integer, LwM2mObjectEnabler> objectEnablers, boolean raiseException)
+            throws IllegalStateException {
+
         LwM2mObjectEnabler securityEnabler = objectEnablers.get(SECURITY);
         LwM2mObjectEnabler serverEnabler = objectEnablers.get(SERVER);
 
@@ -70,74 +76,91 @@ public class ServersInfoExtractor {
         LwM2mObject servers = (LwM2mObject) serverEnabler.read(SYSTEM, new ReadRequest(SERVER)).getContent();
 
         for (LwM2mObjectInstance security : securities.getInstances().values()) {
-            try {
-                if ((boolean) security.getResource(SEC_BOOTSTRAP).getValue()) {
-                    if (infos.bootstrap != null) {
-                        LOG.warn("There is more than one bootstrap configuration in security object.");
-                    } else {
-                        // create bootstrap info
-                        ServerInfo info = new ServerInfo();
-                        info.bootstrap = true;
-                        LwM2mResource serverIdResource = security.getResource(SEC_SERVER_ID);
-                        if (serverIdResource != null && serverIdResource.getValue() != null)
-                            info.serverId = (long) serverIdResource.getValue();
-                        else
-                            info.serverId = 0;
-                        info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue());
-                        info.secureMode = getSecurityMode(security);
-                        if (info.secureMode == SecurityMode.PSK) {
-                            info.pskId = getPskIdentity(security);
-                            info.pskKey = getPskKey(security);
-                        } else if (info.secureMode == SecurityMode.RPK) {
-                            info.publicKey = getPublicKey(security);
-                            info.privateKey = getPrivateKey(security);
-                            info.serverPublicKey = getServerPublicKey(security);
-                        } else if (info.secureMode == SecurityMode.X509) {
-                            info.clientCertificate = getClientCertificate(security);
-                            info.serverCertificate = getServerCertificate(security);
-                            info.privateKey = getPrivateKey(security);
-                            info.certificateUsage = getCertificateUsage(security);
-                        }
-                        infos.bootstrap = info;
+            if ((boolean) security.getResource(SEC_BOOTSTRAP).getValue()) {
+                if (infos.bootstrap != null) {
+                    String message = "There is more than one bootstrap configuration in security object.";
+                    LOG.debug(message);
+                    if (raiseException) {
+                        throw new IllegalStateException(message);
                     }
                 } else {
-                    // create device management info
-                    DmServerInfo info = new DmServerInfo();
-                    info.bootstrap = false;
-                    info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue());
-                    info.serverId = (long) security.getResource(SEC_SERVER_ID).getValue();
-                    info.secureMode = getSecurityMode(security);
-                    if (info.secureMode == SecurityMode.PSK) {
-                        info.pskId = getPskIdentity(security);
-                        info.pskKey = getPskKey(security);
-                    } else if (info.secureMode == SecurityMode.RPK) {
-                        info.publicKey = getPublicKey(security);
-                        info.privateKey = getPrivateKey(security);
-                        info.serverPublicKey = getServerPublicKey(security);
-                    } else if (info.secureMode == SecurityMode.X509) {
-                        info.clientCertificate = getClientCertificate(security);
-                        info.serverCertificate = getServerCertificate(security);
-                        info.privateKey = getPrivateKey(security);
-                        info.certificateUsage = getCertificateUsage(security);
-                    }
-                    // search corresponding device management server
-                    for (LwM2mObjectInstance server : servers.getInstances().values()) {
-                        if (info.serverId == (Long) server.getResource(SRV_SERVER_ID).getValue()) {
-                            info.lifetime = (long) server.getResource(SRV_LIFETIME).getValue();
-                            info.binding = BindingMode.parse((String) server.getResource(SRV_BINDING).getValue());
-
-                            infos.deviceManagements.put(info.serverId, info);
-                            break;
-                        }
+                    // create server info for bootstrap server
+                    ServerInfo serverInfo = new ServerInfo();
+                    serverInfo.bootstrap = true;
+                    try {
+                        // fill info from current client state.
+                        populateServerInfo(serverInfo, security);
+
+                        // add server info to result to return
+                        infos.bootstrap = serverInfo;
+                    } catch (RuntimeException e) {
+                        LOG.debug("Unable to get info for bootstrap server /O/{}", security.getId(), e);
+                        if (raiseException)
+                            throw e;
                     }
                 }
-            } catch (URISyntaxException e) {
-                LOG.error(String.format("Invalid URI %s", (String) security.getResource(SEC_SERVER_URI).getValue()), e);
+            } else {
+                try {
+                    // create device management info
+                    DmServerInfo info = createDMServerInfo(security, servers);
+                    infos.deviceManagements.put(info.serverId, info);
+                } catch (RuntimeException e) {
+                    LOG.debug("Unable to get info for DM server /O/{}", security.getId(), e);
+                    if (raiseException)
+                        throw e;
+                }
             }
         }
         return infos;
     }
 
+    private static void populateServerInfo(ServerInfo info, LwM2mObjectInstance security) {
+        try {
+            LwM2mResource serverIdResource = security.getResource(SEC_SERVER_ID);
+            if (serverIdResource != null && serverIdResource.getValue() != null)
+                info.serverId = (long) serverIdResource.getValue();
+            else
+                info.serverId = 0;
+            info.serverUri = new URI((String) security.getResource(SEC_SERVER_URI).getValue());
+            info.secureMode = getSecurityMode(security);
+            if (info.secureMode == SecurityMode.PSK) {
+                info.pskId = getPskIdentity(security);
+                info.pskKey = getPskKey(security);
+            } else if (info.secureMode == SecurityMode.RPK) {
+                info.publicKey = getPublicKey(security);
+                info.privateKey = getPrivateKey(security);
+                info.serverPublicKey = getServerPublicKey(security);
+            } else if (info.secureMode == SecurityMode.X509) {
+                info.clientCertificate = getClientCertificate(security);
+                info.serverCertificate = getServerCertificate(security);
+                info.privateKey = getPrivateKey(security);
+                info.certificateUsage = getCertificateUsage(security);
+            }
+        } catch (RuntimeException | URISyntaxException e) {
+            throw new IllegalStateException("Invalid Security Instance /0/" + security.getId(), e);
+        }
+    }
+
+    private static DmServerInfo createDMServerInfo(LwM2mObjectInstance security, LwM2mObject servers) {
+        DmServerInfo info = new DmServerInfo();
+        info.bootstrap = false;
+        populateServerInfo(info, security);
+
+        // search corresponding device management server
+        for (LwM2mObjectInstance server : servers.getInstances().values()) {
+            try {
+                if (info.serverId == (Long) server.getResource(SRV_SERVER_ID).getValue()) {
+                    info.lifetime = (long) server.getResource(SRV_LIFETIME).getValue();
+                    info.binding = BindingMode.parse((String) server.getResource(SRV_BINDING).getValue());
+                    return info;
+                }
+            } catch (RuntimeException e) {
+                throw new IllegalStateException("Invalid Server Instance /1/" + server.getId(), e);
+            }
+        }
+        return null;
+    }
+
     public static DmServerInfo getDMServerInfo(Map<Integer, LwM2mObjectEnabler> objectEnablers, Long shortID) {
         ServersInfo info = getInfo(objectEnablers);
         if (info == null)
@@ -208,6 +231,18 @@ public class ServersInfoExtractor {
         }
     }
 
+    public static String getServerURI(LwM2mObjectEnabler objectEnabler, int instanceId) {
+        ReadResponse response = null;
+        if (objectEnabler.getId() == SECURITY) {
+            response = objectEnabler.read(ServerIdentity.SYSTEM, new ReadRequest(SECURITY, instanceId, SEC_SERVER_URI));
+        }
+        if (response != null && response.isSuccess()) {
+            return (String) ((LwM2mResource) response.getContent()).getValue();
+        } else {
+            return null;
+        }
+    }
+
     public static SecurityMode getSecurityMode(LwM2mObjectInstance securityInstance) {
         return SecurityMode.fromCode((long) securityInstance.getResource(SEC_SECURITY_MODE).getValue());
     }
@@ -233,11 +268,10 @@ public class ServersInfoExtractor {
             KeyFactory kf = KeyFactory.getInstance(algorithm);
             return kf.generatePublic(keySpec);
         } catch (NoSuchAlgorithmException e) {
-            LOG.debug("Failed to instantiate key factory for algorithm " + algorithm, e);
+            throw new IllegalArgumentException("Failed to instantiate key factory for algorithm " + algorithm, e);
         } catch (InvalidKeySpecException e) {
-            LOG.debug("Failed to decode RFC7250 public key with algorithm " + algorithm, e);
+            throw new IllegalArgumentException("Failed to decode RFC7250 public key with algorithm " + algorithm, e);
         }
-        return null;
     }
 
     private static PrivateKey getPrivateKey(LwM2mObjectInstance securityInstance) {
@@ -245,8 +279,7 @@ public class ServersInfoExtractor {
         try {
             return SecurityUtil.privateKey.decode(encodedKey);
         } catch (IOException | GeneralSecurityException e) {
-            LOG.debug("Failed to decode RFC5958 private key", e);
-            return null;
+            throw new IllegalArgumentException("Failed to decode RFC5958 private key", e);
         }
     }
 
@@ -255,8 +288,7 @@ public class ServersInfoExtractor {
         try {
             return SecurityUtil.publicKey.decode(encodedKey);
         } catch (IOException | GeneralSecurityException e) {
-            LOG.debug("Failed to decode RFC7250 public key", e);
-            return null;
+            throw new IllegalArgumentException("Failed to decode RFC7250 public key", e);
         }
     }
 
@@ -265,8 +297,7 @@ public class ServersInfoExtractor {
         try {
             return SecurityUtil.certificate.decode(encodedCert);
         } catch (IOException | GeneralSecurityException e) {
-            LOG.debug("Failed to decode X.509 certificate", e);
-            return null;
+            throw new IllegalArgumentException("Failed to decode X.509 certificate", e);
         }
     }
 
@@ -278,8 +309,7 @@ public class ServersInfoExtractor {
                 return cf.generateCertificate(in);
             }
         } catch (CertificateException | IOException e) {
-            LOG.debug("Failed to decode X.509 certificate", e);
-            return null;
+            throw new IllegalArgumentException("Failed to decode X.509 certificate", e);
         }
     }
 
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
index 4c67b126..d8c1d51b 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java
@@ -170,21 +170,29 @@ public final class LinkFormatHelper {
         // add instance link
         for (Integer instanceId : objectEnabler.getAvailableInstanceIds()) {
             String instanceURL = getPath("/", Integer.toString(objectEnabler.getId()), Integer.toString(instanceId));
+            Map<String, String> objectAttributes = new HashMap<>();
 
             // get short id
-            Long shortServerId = null;
             if (objectEnabler.getId() == LwM2mId.SECURITY || objectEnabler.getId() == LwM2mId.SERVER) {
                 Boolean isBootstrapServer = objectEnabler.getId() == LwM2mId.SECURITY
                         && ServersInfoExtractor.isBootstrapServer(objectEnabler, instanceId);
                 if (isBootstrapServer != null && !isBootstrapServer) {
-                    shortServerId = ServersInfoExtractor.getServerId(objectEnabler, instanceId);
+                    Long shortServerId = ServersInfoExtractor.getServerId(objectEnabler, instanceId);
+                    if (shortServerId != null)
+                        objectAttributes.put("ssid", shortServerId.toString());
                 }
+
+            }
+
+            // get uri
+            if (objectEnabler.getId() == LwM2mId.SECURITY) {
+                String uri = ServersInfoExtractor.getServerURI(objectEnabler, instanceId);
+                if (uri != null)
+                    objectAttributes.put("uri", "\"" + uri + "\"");
             }
 
             // create link
-            if (shortServerId != null) {
-                Map<String, String> objectAttributes = new HashMap<>();
-                objectAttributes.put("ssid", shortServerId.toString());
+            if (!objectAttributes.isEmpty()) {
                 links.add(new Link(instanceURL, objectAttributes));
             } else {
                 links.add(new Link(instanceURL));
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java b/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java
new file mode 100644
index 00000000..adef7947
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/MatchingType.java
@@ -0,0 +1,52 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ *
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ *
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core;
+
+import org.eclipse.leshan.core.util.datatype.ULong;
+
+/**
+ * The Matching Type Resource specifies how the certificate or raw public key in in the Server Public Key is presented.
+ * Four values are currently defined:
+ * <ul>
+ * <li>0: Exact match. This is the default value and also corresponds to the functionality of LwM2M v1.0. Hence, if this
+ * resource is not present then the content of the Server Public Key Resource corresponds to this value.
+ * <li>1: SHA-256 hash [RFC6234]
+ * <li>2: SHA-384 hash [RFC6234]
+ * <li>3: SHA-512 hash [RFC6234]
+ * </ul>
+ */
+public enum MatchingType {
+    EXACT_MATCH(0), SHA256(1), SHA384(2), SHA512(3);
+
+    public final ULong code;
+
+    private MatchingType(int code) {
+        this.code = ULong.valueOf(code);
+    }
+
+    public static MatchingType fromCode(int code) {
+        return fromCode(ULong.valueOf(code));
+    }
+
+    public static MatchingType fromCode(ULong code) {
+        for (MatchingType sm : MatchingType.values()) {
+            if (sm.code.equals(code)) {
+                return sm;
+            }
+        }
+        throw new IllegalArgumentException(String.format("Unsupported MatchingType code : %s", code));
+    }
+}
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java
index ce0e3673..9818dc11 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServer.java
@@ -31,6 +31,8 @@ import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionDispatcher;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
 import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender;
 import org.eclipse.leshan.server.californium.RootResource;
@@ -49,6 +51,7 @@ public class LeshanBootstrapServer {
     private final CoapServer coapServer;
     private final CoapEndpoint unsecuredEndpoint;
     private final CoapEndpoint securedEndpoint;
+    private final BootstrapSessionDispatcher dispatcher = new BootstrapSessionDispatcher();
 
     private LwM2mBootstrapRequestSender requestSender;
 
@@ -90,7 +93,8 @@ public class LeshanBootstrapServer {
         requestSender = createRequestSender(securedEndpoint, unsecuredEndpoint, encoder, decoder);
 
         // create bootstrap resource
-        CoapResource bsResource = createBootstrapResource(bsHandlerFactory.create(requestSender, bsSessionManager));
+        CoapResource bsResource = createBootstrapResource(
+                bsHandlerFactory.create(requestSender, bsSessionManager, dispatcher));
         coapServer.add(bsResource);
     }
 
@@ -177,6 +181,14 @@ public class LeshanBootstrapServer {
         }
     }
 
+    public void addListener(BootstrapSessionListener listener) {
+        dispatcher.addListener(listener);
+    }
+
+    public void removeListener(BootstrapSessionListener listener) {
+        dispatcher.removeListener(listener);
+    }
+
     /**
      * <p>
      * A CoAP API, generally needed when need to access to underlying CoAP protocol.
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java
index e937a296..b1507564 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerBuilder.java
@@ -45,6 +45,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfigStoreTaskProvider;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
 import org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler;
 import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSessionManager;
@@ -401,8 +402,8 @@ public class LeshanBootstrapServerBuilder {
             bootstrapHandlerFactory = new BootstrapHandlerFactory() {
                 @Override
                 public BootstrapHandler create(LwM2mBootstrapRequestSender sender,
-                        BootstrapSessionManager sessionManager) {
-                    return new DefaultBootstrapHandler(sender, sessionManager);
+                        BootstrapSessionManager sessionManager, BootstrapSessionListener listener) {
+                    return new DefaultBootstrapHandler(sender, sessionManager, listener);
                 }
             };
         if (configStore == null) {
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
index 81d35792..bd51afc8 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java
@@ -23,8 +23,12 @@ import java.util.List;
 import java.util.Map;
 
 import org.eclipse.leshan.core.CertificateUsage;
+import org.eclipse.leshan.core.MatchingType;
 import org.eclipse.leshan.core.SecurityMode;
 import org.eclipse.leshan.core.request.BindingMode;
+import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.util.datatype.ULong;
 
 /**
  * A client configuration to apply to a device during a bootstrap session.
@@ -43,6 +47,27 @@ import org.eclipse.leshan.core.request.BindingMode;
  */
 public class BootstrapConfig {
 
+    /**
+     * If activated, bootstrap server will start the bootstrap session with a {@link BootstrapDiscoverRequest} to know
+     * what is the Security(object 0) instance ID of bootstrap server on the device, then it will use this information
+     * to automatically attribute IDs to {@link #security} instance. (meaning that ID defined in {@link #security} will
+     * not be used)
+     * <p>
+     * This can be useful because bootstrap server can not be deleted and only 1 Bootstrap server can be present in
+     * Security Object. <br>
+     * See <a href=
+     * "https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/522">OMA_LwM2M_for_Developers#522</a>for
+     * more details .
+     */
+    public boolean autoIdForSecurityObject = false;
+
+    /**
+     * Content format used to send requests.
+     * <p>
+     * if <code>null</code> content format used is the preferred one by the client (pct attribute from BootstrapRequest)
+     */
+    public ContentFormat contentFormat = null;
+
     /**
      * List of LWM2M path to delete.
      */
@@ -103,12 +128,49 @@ public class BootstrapConfig {
          */
         public EnumSet<BindingMode> binding = EnumSet.of(BindingMode.U);
 
+        /**
+         * If this resource is defined, it provides a link to the APN connection profile Object Instance (OMNA
+         * registered Object ID:11) to be used to communicate with this server.
+         * <p>
+         * Since Server v1.1
+         */
+        public Integer apnLink = null;
+
+        /**
+         * Using the Trigger Resource a LwM2M Client can indicate whether it is reachable over SMS (value set to 'true')
+         * or not (value set to 'false'). The default value (resource not present) is 'false'. When set to 'true' the
+         * LwM2M Server MAY, for example, request the LwM2M Client to perform operations, such as the "Update" operation
+         * by sending an "Execute" operation on "Registration Update Trigger" Resource via SMS. No SMS response is
+         * expected for such a message.
+         * <p>
+         * Since Server v1.1
+         */
+        public Boolean trigger = null;
+
+        /**
+         * Only a single transport binding SHALL be present. When the LwM2M client supports multiple transports, it MAY
+         * use this transport to initiate a connection. This resource can also be used to switch between multiple
+         * transports e.g. a non-IP device can switch to UDP transport to perform firmware updates.
+         * <p>
+         * Since Server v1.1
+         */
+        public BindingMode preferredTransport = null;
+        /**
+         * If true or the Resource is not present, the LwM2M Client Send command capability is de-activated. If false,
+         * the LwM2M Client Send Command capability is activated. *
+         * <p>
+         * Since Server v1.1
+         */
+        public Boolean muteSend = null;
+
         @Override
         public String toString() {
             return String.format(
-                    "ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s]",
-                    shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, binding);
+                    "ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s, apnLink=%s, trigger=%s, preferredTransport=%s, muteSend=%s]",
+                    shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, binding,
+                    apnLink, trigger, preferredTransport, muteSend);
         }
+
     }
 
     /**
@@ -219,6 +281,29 @@ public class BootstrapConfig {
          */
         public Integer bootstrapServerAccountTimeout = 0;
 
+        /**
+         * The Matching Type Resource specifies how the certificate or raw public key in in the Server Public Key is
+         * presented. Four values are currently defined:
+         * <ul>
+         * <li>0: Exact match. This is the default value and also corresponds to the functionality of LwM2M v1.0. Hence,
+         * if this resource is not present then the content of the Server Public Key Resource corresponds to this value.
+         * <li>1: SHA-256 hash [RFC6234]
+         * <li>2: SHA-384 hash [RFC6234]
+         * <li>3: SHA-512 hash [RFC6234]
+         * </ul>
+         * Since Security v1.1
+         */
+        public MatchingType matchingType = null;
+
+        /**
+         * This resource holds the value of the Server Name Indication (SNI) value to be used during the TLS handshake.
+         * When this resource is present then the LwM2M Server URI acts as the address of the service while the SNI
+         * value is used for matching a presented certificate, or PSK identity. *
+         * <p>
+         * Since Security v1.1
+         */
+        public String sni = null;
+
         /**
          * The Certificate Usage Resource specifies the semantic of the certificate or raw public key stored in the
          * Server Public Key Resource, which is used to match the certificate presented in the TLS/DTLS handshake.
@@ -228,17 +313,38 @@ public class BootstrapConfig {
          * <li>2: trust anchor assertion
          * <li>3: domain-issued certificate (default if missing)
          * </ul>
+         * <p>
+         * Since Security v1.1
+         */
+        public CertificateUsage certificateUsage = null;
+
+        /**
+         * When this resource is present it instructs the TLS/DTLS client to propose the indicated ciphersuite(s) in the
+         * ClientHello of the handshake. A ciphersuite is indicated as a 32-bit integer value. The IANA TLS ciphersuite
+         * registry is maintained at https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml. As an
+         * example, the TLS_PSK_WITH_AES_128_CCM_8 ciphersuite is represented with the following string "0xC0,0xA8". To
+         * form an integer value the two values are concatenated. In this example, the value is 0xc0a8 or 49320.
+         * <p>
+         * Since Security v1.1
+         */
+        public ULong cipherSuite = null;
+
+        /**
+         * The Object ID of the OSCORE Object Instance that holds the OSCORE configuration to be used by the LWM2M
+         * Client to the LWM2M Server associated with this Security object.
+         * 
          */
-        public CertificateUsage certificateUsage;
+        public Integer oscoreSecurityMode = null;
 
         @Override
         public String toString() {
             // Note : secretKey and smsBindingKeySecret are explicitly excluded from the display for security purposes
             return String.format(
-                    "ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeySecret=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s, certificateUsage=%s]",
+                    "ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeyParam=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s, matchingType=%s, certificateUsage=%s, sni=%s, cipherSuite=%s, oscoreSecurityMode=%s]",
                     uri, bootstrapServer, securityMode, Arrays.toString(publicKeyOrId),
                     Arrays.toString(serverPublicKey), smsSecurityMode, Arrays.toString(smsBindingKeyParam),
-                    serverSmsNumber, serverId, clientOldOffTime, bootstrapServerAccountTimeout, certificateUsage);
+                    serverSmsNumber, serverId, clientOldOffTime, bootstrapServerAccountTimeout, matchingType,
+                    certificateUsage, sni, cipherSuite, oscoreSecurityMode);
         }
     }
 
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java
index 608bef4d..e153fd0e 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfigStoreTaskProvider.java
@@ -15,10 +15,17 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.bootstrap;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
+import org.eclipse.leshan.core.Link;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
+import org.eclipse.leshan.core.response.BootstrapDiscoverResponse;
 import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * An implementation of {@link BootstrapTaskProvider} which use a {@link BootstrapConfigStore} to know which requests to
@@ -26,6 +33,8 @@ import org.eclipse.leshan.core.response.LwM2mResponse;
  */
 public class BootstrapConfigStoreTaskProvider implements BootstrapTaskProvider {
 
+    private static final Logger LOG = LoggerFactory.getLogger(BootstrapConfigStoreTaskProvider.class);
+
     private BootstrapConfigStore store;
 
     public BootstrapConfigStoreTaskProvider(BootstrapConfigStore store) {
@@ -39,18 +48,73 @@ public class BootstrapConfigStoreTaskProvider implements BootstrapTaskProvider {
         if (config == null)
             return null;
 
-        Tasks tasks = new Tasks();
-        // create requests from config
-        tasks.requestsToSend = BootstrapUtil.toRequests(config, session.getContentFormat());
+        if (previousResponse == null && shouldStartWithDiscover(config)) {
+            Tasks tasks = new Tasks();
+            tasks.requestsToSend = new ArrayList<>(1);
+            tasks.requestsToSend.add(new BootstrapDiscoverRequest());
+            tasks.last = false;
+            return tasks;
+        } else {
+            Tasks tasks = new Tasks();
+
+            // handle bootstrap discover response
+            if (previousResponse != null) {
+                BootstrapDiscoverResponse response = (BootstrapDiscoverResponse) previousResponse.get(0);
+                if (!response.isSuccess()) {
+                    LOG.warn(
+                            "Bootstrap Discover return error {} : unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
+                            response, session);
+                    return null;
+                }
+
+                Integer bootstrapServerInstanceId = findBootstrapServerInstanceId(response.getObjectLinks());
+                if (bootstrapServerInstanceId == null) {
+                    LOG.warn(
+                            "Unable to find bootstrap server instance in Security Object (0) in response {}: unable to continue bootstrap session with autoIdForSecurityObject mode. {}",
+                            response, session);
+                    return null;
+                }
+
+                // create requests from config
+                tasks.requestsToSend = BootstrapUtil.toRequests(config,
+                        config.contentFormat != null ? config.contentFormat : session.getContentFormat(),
+                        bootstrapServerInstanceId);
+            } else {
+                // create requests from config
+                tasks.requestsToSend = BootstrapUtil.toRequests(config,
+                        config.contentFormat != null ? config.contentFormat : session.getContentFormat());
+
+            }
 
-        // We add model for Security(0), Server(0) and ACL(2) which are the only one supported by BootstrapConfig
-        // We use default 1.0 model as currently BootstrapConfig support only this model version (which should be
-        // compatible with models of LWM2M v1.1 but without new resources)
-        tasks.supportedObjects = new HashMap<>();
-        tasks.supportedObjects.put(0, "1.0");
-        tasks.supportedObjects.put(1, "1.0");
-        tasks.supportedObjects.put(2, "1.0");
+            // We add model for Security(0), Server(0) and ACL(2) which are the only one supported by BootstrapConfig
+            tasks.supportedObjects = new HashMap<>();
+            tasks.supportedObjects.put(0, "1.1");
+            tasks.supportedObjects.put(1, "1.1");
+            tasks.supportedObjects.put(2, "1.0");
+
+            return tasks;
+        }
+    }
+
+    protected boolean shouldStartWithDiscover(BootstrapConfig config) {
+        return config.autoIdForSecurityObject;
+    }
 
-        return tasks;
+    protected Integer findBootstrapServerInstanceId(Link[] objectLinks) {
+        for (Link link : objectLinks) {
+            if (link.getUrl().startsWith("/0/")) {
+                try {
+                    LwM2mPath path = new LwM2mPath(link.getUrl());
+                    if (path.isObjectInstance()) {
+                        if (!link.getAttributes().containsKey("ssid"))
+                            return path.getObjectInstanceId();
+                    }
+                } catch (Exception e) {
+                    // ignore if this is not a LWM2M path
+                    LOG.warn("Invalid LwM2MPath starting by \"/0/\"");
+                }
+            }
+        }
+        return null;
     }
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java
index da38072e..c65b3c10 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerFactory.java
@@ -27,7 +27,9 @@ public interface BootstrapHandlerFactory {
      * 
      * @param sender the class responsible to send LWM2M request during a bootstapSession.
      * @param sessionManager the manager responsible to handle bootstrap session.
+     * @param listener a listener of bootstrap session life-cycle.
      * @return the new {@link BootstrapHandler}.
      */
-    BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager);
+    BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager,
+            BootstrapSessionListener listener);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java
new file mode 100644
index 00000000..e6930068
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionAdapter.java
@@ -0,0 +1,67 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+
+public class BootstrapSessionAdapter implements BootstrapSessionListener {
+
+    @Override
+    public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) {
+    }
+
+    @Override
+    public void unAuthorized(BootstrapRequest request, Identity clientIdentity) {
+    }
+
+    @Override
+    public void authorized(BootstrapSession session) {
+    }
+
+    @Override
+    public void noConfig(BootstrapSession session) {
+    }
+
+    @Override
+    public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request) {
+    }
+
+    @Override
+    public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+    }
+
+    @Override
+    public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+    }
+
+    @Override
+    public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            Throwable cause) {
+    }
+
+    @Override
+    public void end(BootstrapSession session) {
+    }
+
+    @Override
+    public void failed(BootstrapSession session, BootstrapFailureCause cause) {
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java
new file mode 100644
index 00000000..ee0fa05c
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionDispatcher.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+
+public class BootstrapSessionDispatcher implements BootstrapSessionListener {
+
+    private List<BootstrapSessionListener> listeners = new CopyOnWriteArrayList<>();
+
+    public void addListener(BootstrapSessionListener listener) {
+        listeners.add(listener);
+    }
+
+    public void removeListener(BootstrapSessionListener listener) {
+        listeners.remove(listener);
+    }
+
+    @Override
+    public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.sessionInitiated(request, clientIdentity);
+        }
+    }
+
+    @Override
+    public void unAuthorized(BootstrapRequest request, Identity clientIdentity) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.unAuthorized(request, clientIdentity);
+        }
+    }
+
+    @Override
+    public void authorized(BootstrapSession session) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.authorized(session);
+        }
+    }
+
+    @Override
+    public void noConfig(BootstrapSession session) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.noConfig(session);
+        }
+    }
+
+    @Override
+    public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.sendRequest(session, request);
+        }
+    }
+
+    @Override
+    public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.onResponseSuccess(session, request, response);
+        }
+    }
+
+    @Override
+    public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.onResponseError(session, request, response);
+        }
+    }
+
+    @Override
+    public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            Throwable cause) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.onRequestFailure(session, request, cause);
+        }
+    }
+
+    @Override
+    public void end(BootstrapSession session) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.end(session);
+        }
+    }
+
+    @Override
+    public void failed(BootstrapSession session, BootstrapFailureCause cause) {
+        for (BootstrapSessionListener listener : listeners) {
+            listener.failed(session, cause);
+        }
+    }
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java
new file mode 100644
index 00000000..e8277a45
--- /dev/null
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapSessionListener.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.server.bootstrap;
+
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+
+public interface BootstrapSessionListener {
+
+    /**
+     * Called when a client try to initiate a session
+     */
+    void sessionInitiated(BootstrapRequest request, Identity clientIdentity);
+
+    /**
+     * Called if client is not authorized to start a bootstrap session
+     */
+    void unAuthorized(BootstrapRequest request, Identity clientIdentity);
+
+    /**
+     * Called if client is authorized to start a bootstrap session
+     */
+    void authorized(BootstrapSession session);
+
+    /**
+     * Called if there is no configuration to apply for this client
+     */
+    void noConfig(BootstrapSession session);
+
+    /**
+     * Called when a request is sent
+     */
+    void sendRequest(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request);
+
+    /**
+     * Called when we receive a successful response to a request.
+     * 
+     * @param session the bootstrap session concerned.
+     * @param request The request for which we get a successful response.
+     * @param response The response received.
+     */
+    void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response);
+
+    /**
+     * Called when we receive a error response to a request.
+     * 
+     * @param session the bootstrap session concerned.
+     * @param request The request for which we get a error response.
+     * @param response The response received.
+     * 
+     */
+    public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response);
+
+    /**
+     * Called when a request failed to be sent.
+     * 
+     * @param session the bootstrap session concerned.
+     * @param request The request which failed to be sent.
+     * @param cause The cause of the failure. Can be null.
+     * 
+     */
+    public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            Throwable cause);
+
+    /**
+     * Called when bootstrap session finished successfully
+     */
+    void end(BootstrapSession session);
+
+    /**
+     * Called when bootstrap session failed
+     */
+    void failed(BootstrapSession session, BootstrapFailureCause cause);
+}
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java
index 85649f9b..eed6ec3c 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapUtil.java
@@ -19,6 +19,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 
 import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
@@ -27,6 +28,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
@@ -41,6 +43,7 @@ public class BootstrapUtil {
     public static LwM2mObjectInstance toSecurityInstance(int instanceId, ServerSecurity securityConfig) {
         Collection<LwM2mResource> resources = new ArrayList<>();
 
+        // resources since v1.0
         if (securityConfig.uri != null)
             resources.add(LwM2mSingleResource.newStringResource(0, securityConfig.uri));
         resources.add(LwM2mSingleResource.newBooleanResource(1, securityConfig.bootstrapServer));
@@ -66,9 +69,20 @@ public class BootstrapUtil {
             resources.add(LwM2mSingleResource.newIntegerResource(11, securityConfig.clientOldOffTime));
         if (securityConfig.bootstrapServerAccountTimeout != null)
             resources.add(LwM2mSingleResource.newIntegerResource(12, securityConfig.bootstrapServerAccountTimeout));
+
+        // resources since v1.1
+        if (securityConfig.matchingType != null)
+            resources.add(LwM2mSingleResource.newUnsignedIntegerResource(13, securityConfig.matchingType.code));
+        if (securityConfig.sni != null)
+            resources.add(LwM2mSingleResource.newStringResource(14, securityConfig.sni));
         if (securityConfig.certificateUsage != null)
             resources.add(LwM2mSingleResource.newUnsignedIntegerResource(15, securityConfig.certificateUsage.code));
-
+        if (securityConfig.cipherSuite != null)
+            resources.add(LwM2mSingleResource.newUnsignedIntegerResource(16, securityConfig.cipherSuite));
+        if (securityConfig.oscoreSecurityMode != null) {
+            resources.add(LwM2mSingleResource.newObjectLinkResource(17,
+                    new ObjectLink(21, securityConfig.oscoreSecurityMode)));
+        }
         return new LwM2mObjectInstance(instanceId, resources);
     }
 
@@ -82,6 +96,7 @@ public class BootstrapUtil {
     public static LwM2mObjectInstance toServerInstance(int instanceId, ServerConfig serverConfig) {
         Collection<LwM2mResource> resources = new ArrayList<>();
 
+        // resources since v1.0
         resources.add(LwM2mSingleResource.newIntegerResource(0, serverConfig.shortId));
         resources.add(LwM2mSingleResource.newIntegerResource(1, serverConfig.lifetime));
         if (serverConfig.defaultMinPeriod != null)
@@ -94,6 +109,16 @@ public class BootstrapUtil {
         if (serverConfig.binding != null)
             resources.add(LwM2mSingleResource.newStringResource(7, BindingMode.toString(serverConfig.binding)));
 
+        // resources since v1.1
+        if (serverConfig.apnLink != null)
+            resources.add(LwM2mSingleResource.newObjectLinkResource(10, new ObjectLink(11, serverConfig.apnLink)));
+        if (serverConfig.trigger != null)
+            resources.add(LwM2mSingleResource.newBooleanResource(21, serverConfig.trigger));
+        if (serverConfig.preferredTransport != null)
+            resources.add(LwM2mSingleResource.newStringResource(22, serverConfig.preferredTransport.toString()));
+        if (serverConfig.muteSend != null)
+            resources.add(LwM2mSingleResource.newBooleanResource(23, serverConfig.muteSend));
+
         return new LwM2mObjectInstance(instanceId, resources);
     }
 
@@ -149,4 +174,34 @@ public class BootstrapUtil {
         }
         return (requests);
     }
+
+    public static List<BootstrapDownlinkRequest<? extends LwM2mResponse>> toRequests(BootstrapConfig bootstrapConfig,
+            ContentFormat contentFormat, int bootstrapServerID) {
+        List<BootstrapDownlinkRequest<? extends LwM2mResponse>> requests = new ArrayList<>();
+        // handle delete
+        for (String path : bootstrapConfig.toDelete) {
+            requests.add(new BootstrapDeleteRequest(path));
+        }
+        // handle security
+        int id = 0;
+        for (ServerSecurity security : new TreeMap<>(bootstrapConfig.security).values()) {
+            if (security.bootstrapServer) {
+                requests.add(toWriteRequest(bootstrapServerID, security, contentFormat));
+            } else {
+                if (id == bootstrapServerID)
+                    id++;
+                requests.add(toWriteRequest(id, security, contentFormat));
+                id++;
+            }
+        }
+        // handle server
+        for (Entry<Integer, ServerConfig> server : bootstrapConfig.servers.entrySet()) {
+            requests.add(toWriteRequest(server.getKey(), server.getValue(), contentFormat));
+        }
+        // handle acl
+        for (Entry<Integer, ACLConfig> acl : bootstrapConfig.acls.entrySet()) {
+            requests.add(toWriteRequest(acl.getKey(), acl.getValue(), contentFormat));
+        }
+        return (requests);
+    }
 }
\ No newline at end of file
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
index 22aef263..402b8162 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/DefaultBootstrapHandler.java
@@ -28,6 +28,7 @@ import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ResponseCallback;
 import org.eclipse.leshan.core.response.SendableResponse;
+import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager.BootstrapPolicy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,15 +56,21 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
 
     protected final ConcurrentHashMap<String, BootstrapSession> onGoingSession = new ConcurrentHashMap<>();
     protected final BootstrapSessionManager sessionManager;
+    protected final BootstrapSessionListener listener;
 
-    public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager) {
-        this(sender, sessionManager, DEFAULT_TIMEOUT);
+    public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager,
+            BootstrapSessionListener listener) {
+        this(sender, sessionManager, listener, DEFAULT_TIMEOUT);
     }
 
     public DefaultBootstrapHandler(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager,
-            long requestTimeout) {
+            BootstrapSessionListener listener, long requestTimeout) {
+        Validate.notNull(sender);
+        Validate.notNull(sessionManager);
+        Validate.notNull(listener);
         this.sender = sender;
         this.sessionManager = sessionManager;
+        this.listener = listener;
         this.requestTimeout = requestTimeout;
     }
 
@@ -74,11 +81,14 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         // Start session, checking the BS credentials
         final BootstrapSession session;
         session = sessionManager.begin(request, sender);
+        listener.sessionInitiated(request, sender);
 
         if (!session.isAuthorized()) {
             sessionManager.failed(session, UNAUTHORIZED);
+            listener.unAuthorized(request, sender);
             return new SendableResponse<>(BootstrapResponse.badRequest("Unauthorized"));
         }
+        listener.authorized(session);
 
         // check if there is not an ongoing session.
         BootstrapSession oldSession = onGoingSession.put(endpoint, session);
@@ -94,6 +104,7 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
             // check if there is a configuration to apply for this device
             if (!sessionManager.hasConfigFor(session)) {
                 LOG.debug("No bootstrap config for {}", session);
+                listener.noConfig(session);
                 stopSession(session, NO_BOOTSTRAP_CONFIG);
                 return new SendableResponse<>(BootstrapResponse.badRequest("no bootstrap config"));
             }
@@ -127,8 +138,10 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
         // if there is no cause of failure, this is a success
         if (cause == null) {
             sessionManager.end(session);
+            listener.end(session);
         } else {
             sessionManager.failed(session, cause);
+            listener.failed(session, cause);
         }
     }
 
@@ -136,16 +149,19 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
     protected void sendRequest(final BootstrapSession session,
             final BootstrapDownlinkRequest<? extends LwM2mResponse> requestToSend) {
 
+        listener.sendRequest(session, requestToSend);
         send(session, requestToSend, new SafeResponseCallback(session) {
             @Override
             public void safeOnResponse(LwM2mResponse response) {
                 if (response.isSuccess()) {
                     LOG.trace("{} receives {} for {}", session, response, requestToSend);
                     BootstrapPolicy policy = sessionManager.onResponseSuccess(session, requestToSend, response);
+                    listener.onResponseSuccess(session, requestToSend, response);
                     afterRequest(session, policy, requestToSend);
                 } else {
                     LOG.debug("{} receives {} for {}", session, response, requestToSend);
                     BootstrapPolicy policy = sessionManager.onResponseError(session, requestToSend, response);
+                    listener.onResponseError(session, requestToSend, response);
                     afterRequest(session, policy, requestToSend);
                 }
             }
@@ -154,6 +170,7 @@ public class DefaultBootstrapHandler implements BootstrapHandler {
             public void safeOnError(Exception e) {
                 LOG.debug("Error for {} while sending {} ", session, requestToSend, e);
                 BootstrapPolicy policy = sessionManager.onRequestFailure(session, requestToSend, e);
+                listener.onRequestFailure(session, requestToSend, e);
                 afterRequest(session, policy, requestToSend);
             }
         });


Test patch:
diff --git a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
index d540a30b..f343bf23 100644
--- a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
+++ b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java
@@ -241,17 +241,21 @@ public class LinkFormatHelperTest {
     @Test
     public void encode_bootstrap_security_object() {
         Map<Integer, LwM2mInstanceEnabler> instancesMap = new HashMap<>();
-        instancesMap.put(0, Security.noSec("coap://localhost", 111));
-        instancesMap.put(1, Security.noSecBootstap("coap://localhost"));
-        instancesMap.put(2, Security.noSec("coap://localhost", 222));
-        instancesMap.put(3, Security.noSec("coap://localhost", 333));
+        instancesMap.put(0, Security.noSec("coap://localhost:11", 111));
+        instancesMap.put(1, Security.noSecBootstap("coap://localhost:1"));
+        instancesMap.put(2, Security.noSec("coap://localhost:22", 222));
+        instancesMap.put(3, Security.noSec("coap://localhost:33", 333));
         ObjectEnabler objectEnabler = new ObjectEnabler(0, getObjectModel(0), instancesMap, null,
                 ContentFormat.DEFAULT);
 
         Link[] links = LinkFormatHelper.getBootstrapObjectDescription(objectEnabler);
         String strLinks = Link.serialize(links);
 
-        assertEquals("</>;lwm2m=1.0,</0/0>;ssid=111,</0/1>,</0/2>;ssid=222,</0/3>;ssid=333", strLinks);
+        assertEquals("</>;lwm2m=1.0,</0/0>;ssid=111;uri=\"coap://localhost:11\"," //
+                + "</0/1>;uri=\"coap://localhost:1\"," //
+                + "</0/2>;ssid=222;uri=\"coap://localhost:22\"," //
+                + "</0/3>;ssid=333;uri=\"coap://localhost:33\"" //
+                , strLinks);
 
     }
 
@@ -261,10 +265,10 @@ public class LinkFormatHelperTest {
 
         // object 0
         Map<Integer, LwM2mInstanceEnabler> securityInstances = new HashMap<>();
-        securityInstances.put(0, Security.noSec("coap://localhost", 111));
-        securityInstances.put(1, Security.noSecBootstap("coap://localhost"));
-        securityInstances.put(2, Security.noSec("coap://localhost", 222));
-        securityInstances.put(3, Security.noSec("coap://localhost", 333));
+        securityInstances.put(0, Security.noSec("coap://localhost:11", 111));
+        securityInstances.put(1, Security.noSecBootstap("coap://localhost:1"));
+        securityInstances.put(2, Security.noSec("coap://localhost:22", 222));
+        securityInstances.put(3, Security.noSec("coap://localhost:33", 333));
         ObjectEnabler securityObjectEnabler = new ObjectEnabler(0, getObjectModel(0), securityInstances, null,
                 ContentFormat.DEFAULT);
         objectEnablers.add(securityObjectEnabler);
@@ -291,9 +295,11 @@ public class LinkFormatHelperTest {
         Link[] links = LinkFormatHelper.getBootstrapClientDescription(objectEnablers);
         String strLinks = Link.serialize(links);
 
-        assertEquals(
-                "</>;lwm2m=1.0,</0/0>;ssid=111,</0/1>,</0/2>;ssid=222,</0/3>;ssid=333,</1>;ver=2.0,</1/0>;ssid=333,</2>;ver=2.0,</3/0>",
-                strLinks);
+        assertEquals("</>;lwm2m=1.0,</0/0>;ssid=111;uri=\"coap://localhost:11\"," //
+                + "</0/1>;uri=\"coap://localhost:1\"," //
+                + "</0/2>;ssid=222;uri=\"coap://localhost:22\"," //
+                + "</0/3>;ssid=333;uri=\"coap://localhost:33\"," //
+                + "</1>;ver=2.0,</1/0>;ssid=333,</2>;ver=2.0,</3/0>", strLinks);
     }
 
     private ObjectModel getObjectModel(int id) {
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
index 98d21ce6..a0e8956d 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
@@ -35,10 +35,16 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.request.BootstrapDiscoverRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ExecuteRequest;
 import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.request.exception.InvalidRequestException;
+import org.eclipse.leshan.core.request.exception.RequestCanceledException;
+import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
 import org.eclipse.leshan.integration.tests.util.BootstrapIntegrationTestHelper;
+import org.eclipse.leshan.integration.tests.util.BootstrapRequestChecker;
 import org.eclipse.leshan.integration.tests.util.TestObjectsInitializer;
+import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause;
 import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException;
 import org.eclipse.leshan.server.security.SecurityInfo;
 import org.junit.After;
@@ -93,6 +99,8 @@ public class BootstrapTest {
         // Create and start bootstrap server
         helper.createBootstrapServer(null);
         helper.bootstrapServer.start();
+        BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker.contentFormatChecker(ContentFormat.TLV);
+        helper.bootstrapServer.addListener(contentFormatChecker);
 
         // Create Client and check it is not already registered
         ContentFormat noPreferredFormat = null; // if no preferred content format server should use TLV
@@ -103,6 +111,7 @@ public class BootstrapTest {
         // Start it and wait for registration
         helper.client.start();
         helper.waitForRegistrationAtServerSide(1);
+        assertTrue("not expected content format used", contentFormatChecker.isValid());
 
         // check the client is registered
         helper.assertClientRegisterered();
@@ -117,6 +126,9 @@ public class BootstrapTest {
         // Create and start bootstrap server
         helper.createBootstrapServer(null);
         helper.bootstrapServer.start();
+        BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker
+                .contentFormatChecker(ContentFormat.SENML_CBOR);
+        helper.bootstrapServer.addListener(contentFormatChecker);
 
         // Create Client and check it is not already registered
         ContentFormat preferredFormat = ContentFormat.SENML_CBOR;
@@ -127,6 +139,34 @@ public class BootstrapTest {
         // Start it and wait for registration
         helper.client.start();
         helper.waitForRegistrationAtServerSide(1);
+        assertTrue("not expected content format used", contentFormatChecker.isValid());
+
+        // check the client is registered
+        helper.assertClientRegisterered();
+    }
+
+    @Test
+    public void bootstrap_contentformat_from_config() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null, helper.unsecuredBootstrap(ContentFormat.SENML_JSON));
+        helper.bootstrapServer.start();
+        BootstrapRequestChecker contentFormatChecker = BootstrapRequestChecker
+                .contentFormatChecker(ContentFormat.SENML_JSON);
+        helper.bootstrapServer.addListener(contentFormatChecker);
+
+        // Create Client and check it is not already registered
+        ContentFormat preferredFormat = ContentFormat.SENML_CBOR;
+        helper.createClient(preferredFormat);
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for registration
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+        assertTrue("not expected content format used", contentFormatChecker.isValid());
 
         // check the client is registered
         helper.assertClientRegisterered();
@@ -158,7 +198,8 @@ public class BootstrapTest {
         helper.assertClientRegisterered();
 
         // assert session contains additional attributes
-        assertEquals(additionalAttributes, helper.lastBootstrapSession.getBootstrapRequest().getAdditionalAttributes());
+        assertEquals(additionalAttributes,
+                helper.getLastBootstrapSession().getBootstrapRequest().getAdditionalAttributes());
     }
 
     @Test
@@ -183,10 +224,67 @@ public class BootstrapTest {
         helper.assertClientRegisterered();
         assertNotNull(helper.lastDiscoverAnswer);
         assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
-        assertEquals("</>;lwm2m=1.0,</0>;ver=1.1,</0/0>,</1>;ver=1.1,</3>;ver=1.1,</3/0>",
+        assertEquals(
+                String.format("</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</1>;ver=1.1,</3>;ver=1.1,</3/0>",
+                        helper.bootstrapServer.getUnsecuredAddress().getHostString(),
+                        helper.bootstrapServer.getUnsecuredAddress().getPort()),
                 Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
     }
 
+    @Test
+    public void bootstrapWithDiscoverOnRootThenRebootstrap() throws InvalidRequestException, InterruptedException {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null, null, new BootstrapDiscoverRequest());
+        helper.bootstrapServer.start();
+
+        // Create Client and check it is not already registered
+        helper.createClient();
+        helper.assertClientNotRegisterered();
+
+        // Start it and wait for registration
+        helper.client.start();
+        helper.waitForBootstrapFinishedAtClientSide(1);
+        helper.waitForRegistrationAtServerSide(1);
+
+        // check the client is registered
+        helper.assertClientRegisterered();
+        assertNotNull(helper.lastDiscoverAnswer);
+        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
+        assertEquals(
+                String.format("</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</1>;ver=1.1,</3>;ver=1.1,</3/0>",
+                        helper.bootstrapServer.getUnsecuredAddress().getHostString(),
+                        helper.bootstrapServer.getUnsecuredAddress().getPort()),
+                Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+
+        // re-bootstrap
+        try {
+            ExecuteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                    new ExecuteRequest("/1/0/9"));
+            assertTrue(response.isSuccess());
+        } catch (RequestCanceledException e) {
+            // request can be cancelled if server does not received the execute response before the de-registration
+            // so we just ignore this error.
+        }
+
+        // wait bootstrap finished
+        helper.waitForBootstrapFinishedAtClientSide(1);
+
+        // check last discover response
+        assertNotNull(helper.lastDiscoverAnswer);
+        assertEquals(ResponseCode.CONTENT, helper.lastDiscoverAnswer.getCode());
+        assertEquals(String.format(
+                "</>;lwm2m=1.0,</0>;ver=1.1,</0/0>;uri=\"coap://%s:%d\",</0/1>;ssid=2222;uri=\"coap://%s:%d\",</1>;ver=1.1,</1/0>;ssid=2222,</3>;ver=1.1,</3/0>",
+                helper.bootstrapServer.getUnsecuredAddress().getHostString(),
+                helper.bootstrapServer.getUnsecuredAddress().getPort(),
+                helper.server.getUnsecuredAddress().getHostString(), helper.server.getUnsecuredAddress().getPort()),
+                Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
+
+    }
+
     @Test
     public void bootstrapWithDiscoverOnDevice() {
         // Create DM Server without security & start it
@@ -212,6 +310,63 @@ public class BootstrapTest {
         assertEquals("</>;lwm2m=1.0,</3>;ver=1.1,</3/0>", Link.serialize(helper.lastDiscoverAnswer.getObjectLinks()));
     }
 
+    @Test
+    public void bootstrap_create_2_bsserver() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null, helper.unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(0));
+        helper.bootstrapServer.start();
+
+        // Create Client with bootstrap server config at /0/10
+        helper.createClient(helper.withoutSecurityAndInstanceId(10), null);
+        helper.assertClientNotRegisterered();
+
+        // Start BS.
+        // Server will delete /0 but Client will not delete /0/10 instance (because bs server is not deletable)
+        // Then a new BS server will be written at /0/0
+        //
+        // So bootstrap should failed because 2 bs server in Security Object is not a valid state.
+        // see https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/523
+        helper.client.start();
+
+        // ensure bootstrap session failed because of invalid state
+        helper.waitForInconsistentStateAtClientSide(1);
+        helper.waitForBootstrapFailureAtServerSide(1);
+        assertEquals(BootstrapFailureCause.FINISH_FAILED, helper.getLastCauseOfBootstrapFailure());
+    }
+
+    @Test
+    public void bootstrap_with_auto_id_for_security_object() {
+        // Create DM Server without security & start it
+        helper.createServer();
+        helper.server.start();
+
+        // Create and start bootstrap server
+        helper.createBootstrapServer(null, helper.unsecuredBootstrapWithAutoID());
+        helper.bootstrapServer.start();
+
+        // Create Client with bootstrap server config at /0/10
+        helper.createClient(helper.withoutSecurityAndInstanceId(10), null);
+        helper.assertClientNotRegisterered();
+
+        // Start BS.
+        // Server will delete /0 but Client will not delete /0/10 instance (because bs server is not deletable)
+        // Then a new BS server will be written at /0/0
+        //
+        // So bootstrap should failed because 2 bs server in Security Object is not a valid state.
+        // see https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/523
+        helper.client.start();
+
+        // ensure bootstrap session succeed
+        helper.waitForBootstrapFinishedAtClientSide(1);
+        helper.waitForBootstrapSuccessAtServerSide(1);
+        helper.waitForRegistrationAtServerSide(1);
+        helper.assertClientRegisterered();
+    }
+
     @Test
     public void bootstrapDeleteSecurity() {
         // Create DM Server without security & start it
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
index 4798c3cb..5eaacfb5 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapIntegrationTestHelper.java
@@ -40,13 +40,15 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import org.eclipse.californium.core.coap.Request;
 import org.eclipse.leshan.client.californium.LeshanClientBuilder;
 import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory;
 import org.eclipse.leshan.client.object.Device;
 import org.eclipse.leshan.client.object.Security;
-import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
+import org.eclipse.leshan.client.object.Server;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
 import org.eclipse.leshan.core.LwM2mId;
 import org.eclipse.leshan.core.SecurityMode;
@@ -65,6 +67,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
 import org.eclipse.leshan.server.bootstrap.BootstrapConfigStoreTaskProvider;
+import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause;
 import org.eclipse.leshan.server.bootstrap.BootstrapSession;
 import org.eclipse.leshan.server.bootstrap.BootstrapTaskProvider;
 import org.eclipse.leshan.server.bootstrap.DefaultBootstrapSession;
@@ -86,9 +89,10 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     public LeshanBootstrapServer bootstrapServer;
     public final PublicKey bootstrapServerPublicKey;
     public final PrivateKey bootstrapServerPrivateKey;
-    public volatile DefaultBootstrapSession lastBootstrapSession;
     public volatile BootstrapDiscoverResponse lastDiscoverAnswer;
 
+    private SynchronousBootstrapListener bootstrapListener = new SynchronousBootstrapListener();
+
     private class TestBootstrapSessionManager extends DefaultBootstrapSessionManager {
 
         public TestBootstrapSessionManager(BootstrapSecurityStore bsSecurityStore,
@@ -106,11 +110,6 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
             assertThat(request.getCoapRequest(), instanceOf(Request.class));
             return super.begin(request, clientIdentity);
         }
-
-        @Override
-        public void end(BootstrapSession bsSession) {
-            lastBootstrapSession = (DefaultBootstrapSession) bsSession;
-        }
     }
 
     public BootstrapIntegrationTestHelper() {
@@ -166,6 +165,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         builder.setSecurityStore(securityStore);
         builder.setSessionManager(new TestBootstrapSessionManager(securityStore, bootstrapStore));
         bootstrapServer = builder.build();
+        setupBootstrapServerMonitoring();
     }
 
     public void createBootstrapServer(BootstrapSecurityStore securityStore, BootstrapConfigStore bootstrapStore,
@@ -189,13 +189,14 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
                     return tasks;
                 } else {
                     lastDiscoverAnswer = (BootstrapDiscoverResponse) previousResponses.get(0);
-                    return super.getTasks(session, previousResponses);
+                    return super.getTasks(session, null);
                 }
             }
         };
         builder.setSecurityStore(securityStore);
         builder.setSessionManager(new TestBootstrapSessionManager(securityStore, taskProvider));
         bootstrapServer = builder.build();
+        setupBootstrapServerMonitoring();
     }
 
     public void createBootstrapServer(BootstrapSecurityStore securityStore) {
@@ -203,10 +204,21 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     }
 
     public Security withoutSecurity() {
+        return withoutSecurityAndInstanceId(null);
+    }
+
+    public Security withoutSecurityAndInstanceId(Integer id) {
         // Create Security Object (with bootstrap server only)
         String bsUrl = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":"
                 + bootstrapServer.getUnsecuredAddress().getPort();
-        return Security.noSecBootstap(bsUrl);
+        Security sec = Security.noSecBootstap(bsUrl);
+        if (id != null)
+            sec.setId(id);
+        return sec;
+    }
+
+    protected void setupBootstrapServerMonitoring() {
+        bootstrapServer.addListener(bootstrapListener);
     }
 
     @Override
@@ -258,7 +270,7 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         initializer.setInstancesForObject(LwM2mId.SECURITY, security);
         initializer.setInstancesForObject(LwM2mId.DEVICE,
                 new Device("Eclipse Leshan", IntegrationTestHelper.MODEL_NUMBER, "12345"));
-        initializer.setClassForObject(LwM2mId.SERVER, DummyInstanceEnabler.class);
+        initializer.setClassForObject(LwM2mId.SERVER, Server.class);
         createClient(initializer, additionalAttributes, preferredContentFormat, supportedContentFormat);
     }
 
@@ -348,6 +360,23 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
     }
 
     public BootstrapConfigStore unsecuredBootstrapStore() {
+        return unsecuredBootstrapStore(0, false, null);
+    }
+
+    public BootstrapConfigStore unsecuredBootstrapStoreWithBsSecurityInstanceIdAt(Integer instanceId) {
+        return unsecuredBootstrapStore(instanceId, false, null);
+    }
+
+    public BootstrapConfigStore unsecuredBootstrapWithAutoID() {
+        return unsecuredBootstrapStore(0, true, null);
+    }
+
+    public BootstrapConfigStore unsecuredBootstrap(ContentFormat format) {
+        return unsecuredBootstrapStore(0, true, format);
+    }
+
+    public BootstrapConfigStore unsecuredBootstrapStore(final Integer bsInstanceId, final boolean autoId,
+            final ContentFormat format) {
         return new BootstrapConfigStore() {
 
             @Override
@@ -355,13 +384,16 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
 
                 BootstrapConfig bsConfig = new BootstrapConfig();
 
+                bsConfig.autoIdForSecurityObject = autoId;
+                bsConfig.contentFormat = format;
+
                 // security for BS server
                 ServerSecurity bsSecurity = new ServerSecurity();
                 bsSecurity.bootstrapServer = true;
                 bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":"
                         + bootstrapServer.getUnsecuredAddress().getPort();
                 bsSecurity.securityMode = SecurityMode.NO_SEC;
-                bsConfig.security.put(0, bsSecurity);
+                bsConfig.security.put(bsInstanceId, bsSecurity);
 
                 // security for DM server
                 ServerSecurity dmSecurity = new ServerSecurity();
@@ -526,6 +558,30 @@ public class BootstrapIntegrationTestHelper extends SecureIntegrationTestHelper
         };
     }
 
+    public void waitForBootstrapSuccessAtServerSide(long timeInSeconds) {
+        try {
+            bootstrapListener.waitForSuccess(timeInSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void waitForBootstrapFailureAtServerSide(long timeInSeconds) {
+        try {
+            bootstrapListener.waitForFailure(timeInSeconds, TimeUnit.SECONDS);
+        } catch (InterruptedException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public DefaultBootstrapSession getLastBootstrapSession() {
+        return (DefaultBootstrapSession) bootstrapListener.getLastSuccessfulSession();
+    }
+
+    public BootstrapFailureCause getLastCauseOfBootstrapFailure() {
+        return bootstrapListener.getLastCauseOfFailure();
+    }
+
     @Override
     public void dispose() {
         super.dispose();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java
new file mode 100644
index 00000000..3a198ce2
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/BootstrapRequestChecker.java
@@ -0,0 +1,61 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.util;
+
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapWriteRequest;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.server.bootstrap.BootstrapSession;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionAdapter;
+
+public class BootstrapRequestChecker extends BootstrapSessionAdapter {
+
+    public interface RequestValidator {
+        boolean validate(BootstrapDownlinkRequest<? extends LwM2mResponse> request);
+    }
+
+    private RequestValidator validator;
+    private boolean valid = true;
+
+    public BootstrapRequestChecker(RequestValidator validator) {
+        this.validator = validator;
+    }
+
+    public boolean isValid() {
+        return valid;
+    }
+
+    @Override
+    public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request) {
+        if (!validator.validate(request)) {
+            valid = false;
+        }
+
+    }
+
+    public static BootstrapRequestChecker contentFormatChecker(final ContentFormat expectedFormat) {
+        return new BootstrapRequestChecker(new RequestValidator() {
+            @Override
+            public boolean validate(BootstrapDownlinkRequest<? extends LwM2mResponse> request) {
+                if (request instanceof BootstrapWriteRequest) {
+                    return ((BootstrapWriteRequest) request).getContentFormat() == expectedFormat;
+                }
+                return true;
+            }
+        });
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
index d505360c..dac832e2 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/IntegrationTestHelper.java
@@ -281,6 +281,14 @@ public class IntegrationTestHelper {
         }
     }
 
+    public void waitForInconsistentStateAtClientSide(long timeInSeconds) {
+        try {
+            assertTrue(clientObserver.waitForInconsistenState(timeInSeconds, TimeUnit.SECONDS));
+        } catch (InterruptedException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     public void ensureNoUpdate(long timeInSeconds) {
         try {
             registrationListener.waitForUpdate(timeInSeconds, TimeUnit.SECONDS);
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java
new file mode 100644
index 00000000..7ec09967
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousBootstrapListener.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.util;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.leshan.core.request.BootstrapDownlinkRequest;
+import org.eclipse.leshan.core.request.BootstrapRequest;
+import org.eclipse.leshan.core.request.Identity;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+import org.eclipse.leshan.server.bootstrap.BootstrapFailureCause;
+import org.eclipse.leshan.server.bootstrap.BootstrapSession;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener;
+
+public class SynchronousBootstrapListener implements BootstrapSessionListener {
+
+    private CountDownLatch finishedLatch = new CountDownLatch(1);
+    private CountDownLatch failedLatch = new CountDownLatch(1);
+    private BootstrapSession lastSucessfulSession;
+    private BootstrapFailureCause lastCause;
+
+    @Override
+    public void sessionInitiated(BootstrapRequest request, Identity clientIdentity) {
+    }
+
+    @Override
+    public void unAuthorized(BootstrapRequest request, Identity clientIdentity) {
+    }
+
+    @Override
+    public void authorized(BootstrapSession session) {
+
+    }
+
+    @Override
+    public void noConfig(BootstrapSession session) {
+    }
+
+    @Override
+    public void sendRequest(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request) {
+    }
+
+    @Override
+    public void onResponseSuccess(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+    }
+
+    @Override
+    public void onResponseError(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            LwM2mResponse response) {
+    }
+
+    @Override
+    public void onRequestFailure(BootstrapSession session, BootstrapDownlinkRequest<? extends LwM2mResponse> request,
+            Throwable cause) {
+    }
+
+    @Override
+    public void end(BootstrapSession session) {
+        lastSucessfulSession = session;
+        finishedLatch.countDown();
+    }
+
+    @Override
+    public void failed(BootstrapSession session, BootstrapFailureCause cause) {
+        lastCause = cause;
+        failedLatch.countDown();
+    }
+
+    /**
+     * Wait until next bootstrap success event.
+     * 
+     * @throws TimeoutException if wait timeouts
+     */
+    public void waitForSuccess(long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
+        try {
+            if (!finishedLatch.await(timeout, timeUnit))
+                throw new TimeoutException("wait for bs success timeout");
+        } finally {
+            finishedLatch = new CountDownLatch(1);
+        }
+    }
+
+    /**
+     * Wait until next bootstrap failure event.
+     * 
+     * @throws TimeoutException if wait timeouts
+     */
+    public void waitForFailure(long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
+        try {
+            if (!failedLatch.await(timeout, timeUnit))
+                throw new TimeoutException("wait for bs failure timeout");
+        } finally {
+            failedLatch = new CountDownLatch(1);
+        }
+    }
+
+    public BootstrapSession getLastSuccessfulSession() {
+        return lastSucessfulSession;
+    }
+
+    public BootstrapFailureCause getLastCauseOfFailure() {
+        return lastCause;
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java
index 79e232ab..cd46e67d 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/SynchronousClientObserver.java
@@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.eclipse.leshan.client.bootstrap.InvalidStateException;
 import org.eclipse.leshan.client.observer.LwM2mClientObserverAdapter;
 import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.ResponseCode;
@@ -44,6 +45,7 @@ public class SynchronousClientObserver extends LwM2mClientObserverAdapter {
 
     private CountDownLatch bootstrapLatch = new CountDownLatch(1);
     private AtomicBoolean bootstrapSucceed = new AtomicBoolean(false);
+    private AtomicBoolean bootstrapInvalidState = new AtomicBoolean(false);
     private AtomicBoolean bootstrapFailed = new AtomicBoolean(false);
 
     @Override
@@ -55,6 +57,9 @@ public class SynchronousClientObserver extends LwM2mClientObserverAdapter {
     @Override
     public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, ResponseCode responseCode,
             String errorMessage, Exception cause) {
+        if (cause instanceof InvalidStateException) {
+            bootstrapInvalidState.set(true);
+        }
         bootstrapFailed.set(true);
         bootstrapLatch.countDown();
     }
@@ -89,8 +94,8 @@ public class SynchronousClientObserver extends LwM2mClientObserverAdapter {
     }
 
     @Override
-    public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage,
-            Exception cause) {
+    public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode,
+            String errorMessage, Exception cause) {
         updateFailed.set(true);
         updateLatch.countDown();
     }
@@ -202,4 +207,26 @@ public class SynchronousClientObserver extends LwM2mClientObserverAdapter {
             bootstrapLatch = new CountDownLatch(1);
         }
     }
+
+    /**
+     * Wait for Invalid consistency check.
+     * 
+     * @return true if the client state is inconsistent
+     * @throws TimeoutException if bootstrap timeout
+     */
+    public boolean waitForInconsistenState(long timeout, TimeUnit timeUnit)
+            throws InterruptedException, TimeoutException {
+        try {
+            if (bootstrapLatch.await(timeout, timeUnit)) {
+                if (bootstrapInvalidState.get())
+                    return true;
+                if (bootstrapFailed.get() || bootstrapSucceed.get())
+                    return false;
+                throw new TimeoutException("client bootstrap timeout");
+            }
+            throw new TimeoutException("client bootstrap latch timeout");
+        } finally {
+            bootstrapLatch = new CountDownLatch(1);
+        }
+    }
 }
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java
index b57dc438..fc004c69 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/bootstrap/LeshanBootstrapServerTest.java
@@ -28,6 +28,7 @@ import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandler;
 import org.eclipse.leshan.server.bootstrap.BootstrapHandlerFactory;
 import org.eclipse.leshan.server.bootstrap.BootstrapSession;
+import org.eclipse.leshan.server.bootstrap.BootstrapSessionListener;
 import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager;
 import org.eclipse.leshan.server.bootstrap.DefaultBootstrapHandler;
 import org.eclipse.leshan.server.bootstrap.LwM2mBootstrapRequestSender;
@@ -43,8 +44,9 @@ public class LeshanBootstrapServerTest {
         builder.setBootstrapHandlerFactory(new BootstrapHandlerFactory() {
 
             @Override
-            public BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager) {
-                bsHandler = new DefaultBootstrapHandler(sender, sessionManager);
+            public BootstrapHandler create(LwM2mBootstrapRequestSender sender, BootstrapSessionManager sessionManager,
+                    BootstrapSessionListener listener) {
+                bsHandler = new DefaultBootstrapHandler(sender, sessionManager, listener);
                 return bsHandler;
             }
         });
diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
index 9a136d20..92d3efd8 100644
--- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
+++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/bootstrap/BootstrapHandlerTest.java
@@ -49,7 +49,8 @@ public class BootstrapHandlerTest {
         // prepare bootstrapHandler with a session manager which does not authorized any session
         BootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(false,
                 new InMemoryBootstrapConfigStore());
-        BootstrapHandler bsHandler = new DefaultBootstrapHandler(null, bsSessionManager);
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(new MockRequestSender(Mode.ALWAYS_SUCCESS),
+                bsSessionManager, new BootstrapSessionDispatcher());
 
         // Try to bootstrap
         BootstrapResponse response = bsHandler
@@ -70,7 +71,8 @@ public class BootstrapHandlerTest {
         bsStore.add("endpoint", new BootstrapConfig());
         MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore);
 
-        BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager);
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager,
+                new BootstrapSessionDispatcher());
 
         // Try to bootstrap
         SendableResponse<BootstrapResponse> sendableResponse = bsHandler
@@ -91,7 +93,8 @@ public class BootstrapHandlerTest {
         EditableBootstrapConfigStore bsStore = new InMemoryBootstrapConfigStore();
         bsStore.add("endpoint", new BootstrapConfig());
         MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore);
-        BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager);
+        BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager,
+                new BootstrapSessionDispatcher());
 
         // Try to bootstrap
         SendableResponse<BootstrapResponse> sendableResponse = bsHandler
@@ -115,7 +118,7 @@ public class BootstrapHandlerTest {
         bsStore.add("endpoint", new BootstrapConfig());
         MockBootstrapSessionManager bsSessionManager = new MockBootstrapSessionManager(true, bsStore);
         BootstrapHandler bsHandler = new DefaultBootstrapHandler(requestSender, bsSessionManager,
-                DefaultBootstrapHandler.DEFAULT_TIMEOUT);
+                new BootstrapSessionDispatcher(), DefaultBootstrapHandler.DEFAULT_TIMEOUT);
 
         // First bootstrap : which will not end (because of sender)
         SendableResponse<BootstrapResponse> first_response = bsHandler