diff --git a/pom.xml b/pom.xml
index acd175a3fdcf4f84add6f0d94c96c5f1944fef6e..8c41bd2b67df7a812a19772297be0cd4f7819da4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,11 @@
       <artifactId>easy-units</artifactId>
       <version>0.0.1-SNAPSHOT</version>
     </dependency>
+    <dependency>
+      <groupId>org.quartz-scheduler</groupId>
+      <artifactId>quartz</artifactId>
+      <version>2.3.2</version> 
+    </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.dataformat</groupId>
       <artifactId>jackson-dataformat-yaml</artifactId>
diff --git a/scripting/bin/administration/diagnostics.py b/scripting/bin/administration/diagnostics.py
new file mode 100755
index 0000000000000000000000000000000000000000..3c1f2eed66b564fb6d73e320bd023e99f32632c6
--- /dev/null
+++ b/scripting/bin/administration/diagnostics.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2018 Research Group Biomedical Physics,
+# Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+#
+"""diagnostics.py
+
+A script which returns a json representation of various parameters which might
+be interesting for debugging the server-side scripting functionality and which
+should not be executable for non-admin users.
+"""
+
+import sys
+
+TEST_MODULES = [
+    "caosdb",
+    "numpy",
+    "pandas",
+]
+
+
+def get_option(name, default=None):
+    for arg in sys.argv:
+        if arg.startswith("--{}=".format(name)):
+            index = len(name) + 3
+            return arg[index:]
+    return default
+
+def get_exit_code():
+    return int(get_option("exit", 0))
+
+def get_auth_token():
+    return get_option("auth-token")
+
+def get_query():
+    return get_option("query")
+
+
+def get_caosdb_info(auth_token):
+    import caosdb as db
+    result = dict()
+
+    try:
+        db.configure_connection(auth_token=auth_token, password_method="auth_token")
+
+        info = db.Info()
+
+        result["info"] = str(info)
+        result["username"] = info.user_info.name
+        result["realm"] = info.user_info.realm
+        result["roles"] = info.user_info.roles
+
+        # execute a query and return the results
+        query = get_query()
+        if query is not None:
+            query_result = db.execute_query(query)
+            result["query"] = (query, str(query_result))
+
+    except Exception as e:
+        result["exception"] = str(e)
+    return result
+
+def test_imports(modules):
+    result = dict()
+    for m in modules:
+        try:
+            i = __import__(m)
+            v = i.__version__ if hasattr(i, "__version__") and i.__version__ is not None else "unknown version"
+            result[m] = (True, v)
+        except ImportError as e:
+            result[m] = (False, str(e))
+    return result
+
+def main():
+    try:
+        import json
+    except ImportError:
+        print('{"python_version":"{v}",'
+              '"python_path":["{p}"]}'.format(v=sys.version,
+                                              p='","'.join(sys.path)))
+        raise
+
+    try:
+        diagnostics = dict()
+        diagnostics["python_version"] = sys.version
+        diagnostics["python_path"] = sys.path
+        diagnostics["call"] = sys.argv
+        diagnostics["import"] = test_imports(TEST_MODULES)
+
+        auth_token = get_auth_token()
+        diagnostics["auth_token"] = auth_token
+
+        if diagnostics["import"]["caosdb"][0] is True:
+            diagnostics["caosdb"] = get_caosdb_info(auth_token)
+
+    finally:
+        json.dump(diagnostics, sys.stdout)
+
+    sys.exit(get_exit_code())
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/main/java/caosdb/server/CaosAuthenticator.java b/src/main/java/caosdb/server/CaosAuthenticator.java
index 9272b41ecf45158440da841415882ef5d7969d31..021f4ab7d4541756df63fa4bf6878fa603179679 100644
--- a/src/main/java/caosdb/server/CaosAuthenticator.java
+++ b/src/main/java/caosdb/server/CaosAuthenticator.java
@@ -24,6 +24,7 @@ package caosdb.server;
 
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.resource.DefaultResource;
+import caosdb.server.utils.ServerMessages;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.AuthenticationToken;
@@ -74,7 +75,7 @@ public class CaosAuthenticator extends Authenticator {
   @Override
   protected int unauthenticated(final Request request, final Response response) {
     final DefaultResource defaultResource =
-        new DefaultResource(AuthenticationUtils.UNAUTHENTICATED.toElement());
+        new DefaultResource(ServerMessages.UNAUTHENTICATED.toElement());
     defaultResource.init(getContext(), request, response);
     defaultResource.handle();
     response.setStatus(org.restlet.data.Status.CLIENT_ERROR_UNAUTHORIZED);
diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java
index ca1e64feec699407b4e35f642522f599440184fd..be705aa58eef7ba045391322d24e267524ebee05 100644
--- a/src/main/java/caosdb/server/CaosDBServer.java
+++ b/src/main/java/caosdb/server/CaosDBServer.java
@@ -84,6 +84,11 @@ import org.apache.shiro.mgt.SecurityManager;
 import org.apache.shiro.subject.Subject;
 import org.apache.shiro.util.Factory;
 import org.apache.shiro.util.ThreadContext;
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.impl.StdSchedulerFactory;
 import org.restlet.Application;
 import org.restlet.Component;
 import org.restlet.Context;
@@ -257,6 +262,7 @@ public class CaosDBServer extends Application {
       throws SecurityException, FileNotFoundException, IOException {
     try {
       init(args);
+      initScheduler();
       initServerProperties();
       initTimeZone();
       OneTimeAuthenticationToken.init();
@@ -357,6 +363,11 @@ public class CaosDBServer extends Application {
     }
   }
 
+  private static void initScheduler() throws SchedulerException {
+    SCHEDULER = StdSchedulerFactory.getDefaultScheduler();
+    SCHEDULER.start();
+  }
+
   private static void initDatatypes(final Access access) throws Exception {
     final RetrieveDatatypes t = new RetrieveDatatypes();
     t.setAccess(access);
@@ -509,6 +520,7 @@ public class CaosDBServer extends Application {
 
   public static final String REQUEST_TIME_LOGGER = "REQUEST_TIME_LOGGER";
   public static final String REQUEST_ERRORS_LOGGER = "REQUEST_ERRORS_LOGGER";
+  private static Scheduler SCHEDULER;
 
   /**
    * Specify the dispatching restlet that maps URIs to their associated resources for processing.
@@ -821,6 +833,10 @@ public class CaosDBServer extends Application {
   public static Properties getServerProperties() {
     return SERVER_PROPERTIES;
   }
+
+  public static void scheduleJob(JobDetail job, Trigger trigger) throws SchedulerException {
+    SCHEDULER.scheduleJob(job, trigger);
+  }
 }
 
 class CaosDBComponent extends Component {
diff --git a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
index 4a52c50196cd61242fd15e94409e8581eabda12a..651aeab6758303bbc0f0419878eaa7a421dbaa21 100644
--- a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
+++ b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
@@ -26,8 +26,6 @@ import static caosdb.server.utils.Utils.URLDecodeWithUTF8;
 
 import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
-import caosdb.server.entity.Message;
-import caosdb.server.entity.Message.MessageType;
 import caosdb.server.permissions.ResponsibleAgent;
 import caosdb.server.permissions.Role;
 import caosdb.server.utils.Utils;
@@ -52,8 +50,6 @@ public class AuthenticationUtils {
   private static final Logger logger = LoggerFactory.getLogger(AuthenticationUtils.class);
 
   public static final String ONE_TIME_TOKEN_COOKIE = "OneTimeToken";
-  public static final Message UNAUTHENTICATED =
-      new Message(MessageType.Error, 401, "Sign up, please!");
   public static final String SESSION_TOKEN_COOKIE = "SessionToken";
   public static final String SESSION_TIMEOUT_COOKIE = "SessionTimeOut";
 
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
index 67e7be5e3171d4d967f7c83335e0512d8a8ac92e..69ab0b546294d9753ee809dfdccafc0c6d708a12 100644
--- a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
@@ -36,11 +36,58 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.subject.Subject;
 import org.eclipse.jetty.util.ajax.JSON;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
+
+class ConsumedInfo {
+
+  private OneTimeAuthenticationToken oneTimeAuthenticationToken;
+  private List<Long> attempts = new LinkedList<>();
+
+  public ConsumedInfo(OneTimeAuthenticationToken oneTimeAuthenticationToken) {
+    this.oneTimeAuthenticationToken = oneTimeAuthenticationToken;
+  }
+
+  public String getKey() {
+    return getKey(oneTimeAuthenticationToken);
+  }
+
+  public static String getKey(OneTimeAuthenticationToken token) {
+    return token.checksum;
+  }
+
+  public int getNoOfAttempts() {
+    return attempts.size();
+  }
+
+  public long getMaxAttempts() {
+    return oneTimeAuthenticationToken.getMaxAttempts();
+  }
+
+  public void consume() {
+    if (getNoOfAttempts() >= getMaxAttempts()) {
+      throw new AuthenticationException("One-token was consumed too often.");
+    }
+    attempts.add(System.currentTimeMillis());
+  }
+}
 
 public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToken {
 
+  private static Map<String, ConsumedInfo> consumedOneTimeTokens = new HashMap<>();
+  private long maxAttempts;
+
   public OneTimeAuthenticationToken(
       final Principal principal,
       final long date,
@@ -49,8 +96,29 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
       final String curry,
       final String checksum,
       final String[] permissions,
-      final String[] roles) {
+      final String[] roles,
+      final long maxAttempts) {
     super(principal, date, timeout, salt, curry, checksum, permissions, roles);
+    this.maxAttempts = maxAttempts;
+    consume();
+  }
+
+  public void consume() {
+    consume(this);
+  }
+
+  public static void consume(OneTimeAuthenticationToken oneTimeAuthenticationToken) {
+    if (oneTimeAuthenticationToken.isValid()) {
+      String key = ConsumedInfo.getKey(oneTimeAuthenticationToken);
+      ConsumedInfo consumedInfo = consumedOneTimeTokens.get(key);
+      if (consumedInfo == null) {
+        consumedInfo = new ConsumedInfo(oneTimeAuthenticationToken);
+        consumedOneTimeTokens.put(key, consumedInfo);
+      }
+      consumedInfo.consume();
+    } else {
+      oneTimeAuthenticationToken.isValid();
+    }
   }
 
   public OneTimeAuthenticationToken(
@@ -58,12 +126,20 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
       final long timeout,
       final String curry,
       final String[] permissions,
-      final String[] roles) {
-    super(principal, timeout, curry, permissions, roles);
+      final String[] roles,
+      final Long maxAttempts) {
+    super(principal, timeout, curry, permissions, roles, maxAttempts != null ? maxAttempts : 1);
   }
 
   private static final long serialVersionUID = -1072740888045267613L;
 
+  /**
+   * Return consumed.
+   *
+   * @param array
+   * @param curry
+   * @return
+   */
   public static OneTimeAuthenticationToken parse(final Object[] array, final String curry) {
     final Principal principal = new Principal((String) array[1], (String) array[2]);
     final String[] roles = toStringArray((Object[]) array[3]);
@@ -72,16 +148,21 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     final long timeout = (Long) array[6];
     final String salt = (String) array[7];
     final String checksum = (String) array[8];
+    final long maxAttempts = (Long) array[9];
     return new OneTimeAuthenticationToken(
-        principal, date, timeout, salt, curry, checksum, permissions, roles);
+        principal, date, timeout, salt, curry, checksum, permissions, roles, maxAttempts);
   }
 
   private static OneTimeAuthenticationToken generate(
-      final Principal principal, final String curry, final String[] permissions, String[] roles) {
-    int timeout =
-        Integer.parseInt(
-            CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS));
-    return new OneTimeAuthenticationToken(principal, timeout, curry, permissions, roles);
+      final Principal principal,
+      final String curry,
+      final String[] permissions,
+      final String[] roles,
+      final long timeout,
+      final long maxAttempts) {
+
+    return new OneTimeAuthenticationToken(
+        principal, timeout, curry, permissions, roles, maxAttempts);
   }
 
   public static List<Config> loadConfig(InputStream input) throws Exception {
@@ -122,17 +203,19 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
   }
 
   public static OneTimeAuthenticationToken generate(Config c, Principal principal, String curry) {
-    return generate(principal, curry, c.getPermissions(), c.getRoles());
+    return generate(
+        principal, curry, c.getPermissions(), c.getRoles(), c.getTimeout(), c.getMaxAttempts());
   }
 
   static Map<String, Config> purposes;
 
-  public static class Output {
+  public static class Output implements Job {
     private String file = null;
+    private String schedule = null;
 
     public Output() {}
 
-    public void output(OneTimeAuthenticationToken t) throws IOException {
+    public static void output(OneTimeAuthenticationToken t, String file) throws IOException {
       try (PrintWriter writer = new PrintWriter(file, "utf-8")) {
         writer.println(t.toString());
       }
@@ -145,6 +228,45 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     public void setFile(String file) {
       this.file = file;
     }
+
+    public String getSchedule() {
+      return schedule;
+    }
+
+    public void setSchedule(String schedule) {
+      this.schedule = schedule;
+    }
+
+    public void init(Config config) throws IOException, SchedulerException {
+
+      if (this.schedule != null) {
+        generate(config); // test config
+        JobDataMap map = new JobDataMap();
+        map.put("config", config);
+        map.put("file", file);
+        JobDetail outputJob = JobBuilder.newJob(Output.class).setJobData(map).build();
+        Trigger trigger =
+            TriggerBuilder.newTrigger()
+                .withIdentity(config.toString())
+                .withSchedule(CronScheduleBuilder.cronSchedule(this.schedule))
+                .build();
+        CaosDBServer.scheduleJob(outputJob, trigger);
+      } else {
+        output(generate(config), file);
+      }
+    }
+
+    @Override
+    public void execute(JobExecutionContext context) throws JobExecutionException {
+      Config config = (Config) context.getMergedJobDataMap().get("config");
+      String file = context.getMergedJobDataMap().getString("file");
+      try {
+        output(generate(config), file);
+      } catch (IOException e) {
+        // TODO log
+        e.printStackTrace();
+      }
+    }
   }
 
   public static class Config {
@@ -152,9 +274,33 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     private String[] roles = {};
     private String purpose = null;
     private Output output = null;
+    private int maxAttempts = 1;
+    private int timeout =
+        Integer.parseInt(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS));
 
     public Config() {}
 
+    public int getTimeout() {
+      return timeout;
+    }
+
+    public void setTimeout(int timeout) {
+      this.timeout = timeout;
+    }
+
+    public void setExpiresAfterSeconds(int seconds) {
+      this.setTimeout(seconds * 1000);
+    }
+
+    public void setMaxAttempts(int maxAttempts) {
+      this.maxAttempts = maxAttempts;
+    }
+
+    public int getMaxAttempts() {
+      return maxAttempts;
+    }
+
     public String[] getPermissions() {
       return permissions;
     }
@@ -194,14 +340,14 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
 
   public static void init(InputStream yamlConfig) throws Exception {
     List<Config> configs = loadConfig(yamlConfig);
-    output(configs);
+    initOutput(configs);
     purposes = getPurposeMap(configs);
   }
 
-  private static void output(List<Config> configs) throws IOException {
+  private static void initOutput(List<Config> configs) throws IOException, SchedulerException {
     for (Config config : configs) {
       if (config.getOutput() != null) {
-        config.getOutput().output(generate(config));
+        config.getOutput().init(config);
       }
     }
   }
@@ -212,6 +358,13 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     }
   }
 
+  @Override
+  protected void setFields(Object[] fields) {
+    if (fields.length == 1) {
+      this.maxAttempts = (long) fields[0];
+    }
+  }
+
   @Override
   public String calcChecksum(String pepper) {
     return calcChecksum(
@@ -224,6 +377,7 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
         this.curry,
         calcChecksum((Object[]) this.permissions),
         calcChecksum((Object[]) this.roles),
+        this.maxAttempts,
         pepper);
   }
 
@@ -239,7 +393,12 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
           this.date,
           this.timeout,
           this.salt,
-          this.checksum
+          this.checksum,
+          this.maxAttempts
         });
   }
+
+  public long getMaxAttempts() {
+    return maxAttempts;
+  }
 }
diff --git a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
index 7d4cd846b5226e61856703c137310fd765643a4b..31192c66d38027a870142b32d58e52800af1fb31 100644
--- a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
@@ -74,7 +74,8 @@ public abstract class SelfValidatingAuthenticationToken extends Principal
       final String[] permissions,
       final String[] roles,
       String checksum,
-      boolean calcChecksum) {
+      boolean calcChecksum,
+      Object... fields) {
     super(principal);
     this.date = date;
     this.timeout = timeout;
@@ -82,15 +83,19 @@ public abstract class SelfValidatingAuthenticationToken extends Principal
     this.curry = curry;
     this.permissions = permissions != null ? permissions : new String[] {};
     this.roles = roles != null ? roles : new String[] {};
+    setFields(fields);
     this.checksum = checksum == null && calcChecksum ? calcChecksum() : checksum;
   }
 
+  protected abstract void setFields(Object[] fields);
+
   public SelfValidatingAuthenticationToken(
       final Principal principal,
       final long timeout,
       final String curry,
       final String[] permissions,
-      final String[] roles) {
+      final String[] roles,
+      Object... fields) {
     this(
         principal,
         System.currentTimeMillis(),
@@ -100,10 +105,11 @@ public abstract class SelfValidatingAuthenticationToken extends Principal
         permissions,
         roles,
         null,
-        true);
+        true,
+        fields);
   }
 
-  public String calcChecksum() {
+  public final String calcChecksum() {
     return calcChecksum(PEPPER);
   }
 
diff --git a/src/main/java/caosdb/server/accessControl/SessionToken.java b/src/main/java/caosdb/server/accessControl/SessionToken.java
index 695e4639c065a2b0ee2d31f05e3dce36cf3a54d3..5f6753afba662dc8db1b1aab539a78ff36585fff 100644
--- a/src/main/java/caosdb/server/accessControl/SessionToken.java
+++ b/src/main/java/caosdb/server/accessControl/SessionToken.java
@@ -86,6 +86,9 @@ public class SessionToken extends SelfValidatingAuthenticationToken {
     return generate((Principal) subject.getPrincipal(), curry, permissions, roles);
   }
 
+  @Override
+  protected void setFields(Object[] fields) {}
+
   @Override
   public String calcChecksum(String pepper) {
     return calcChecksum(
diff --git a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
index ff7e59b6ea4575a3ad95264c4f3dfbbcde41b78b..cce2850d6d532ae8880d208682b5677dc0369089 100644
--- a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
+++ b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
@@ -43,7 +43,7 @@ public class SessionTokenRealm extends AuthenticatingRealm {
   }
 
   public SessionTokenRealm() {
-    setAuthenticationTokenClass(SessionToken.class);
+    setAuthenticationTokenClass(SelfValidatingAuthenticationToken.class);
     setCredentialsMatcher(new AllowAllCredentialsMatcher());
     setCachingEnabled(false);
     setAuthenticationCachingEnabled(false);
diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
index 013501a0f39b24638c27a162e24015c168f13f3e..1d87c4608e7be3ace4aca09eff4d5e0621cc6b9f 100644
--- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
+++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
@@ -209,13 +209,11 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
 
     if (user != null && user.isAuthenticated()) {
       Element userInfo = new Element("UserInfo");
-      if (!user.getPrincipal().equals(AuthenticationUtils.ANONYMOUS_USER.getPrincipal())) {
-        // TODO: deprecated
-        addNameAndRealm(retRoot, user);
+      // TODO: deprecated, needs refactoring in the webui first
+      addNameAndRealm(retRoot, user);
 
-        // this is the new, correct way
-        addNameAndRealm(userInfo, user);
-      }
+      // this is the new, correct way
+      addNameAndRealm(userInfo, user);
 
       addRoles(userInfo, user);
       retRoot.addContent(userInfo);
@@ -408,11 +406,9 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
       getRequest().getAttributes().put("THROWN", t);
       throw t;
     } catch (final AuthenticationException e) {
-      getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN);
-      return null;
+      return error(ServerMessages.UNAUTHENTICATED, Status.CLIENT_ERROR_UNAUTHORIZED);
     } catch (final AuthorizationException e) {
-      getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN);
-      return null;
+      return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN);
     } catch (final Message m) {
       return error(m, Status.CLIENT_ERROR_BAD_REQUEST);
     } catch (final FileUploadException e) {
diff --git a/src/main/java/caosdb/server/resource/FileSystemResource.java b/src/main/java/caosdb/server/resource/FileSystemResource.java
index ecaa97fe93cb59fe6a4c58fb60d99a5dca373080..ef352e19eb178c5cefbdbd70d2e48b3231ac7bd7 100644
--- a/src/main/java/caosdb/server/resource/FileSystemResource.java
+++ b/src/main/java/caosdb/server/resource/FileSystemResource.java
@@ -135,8 +135,9 @@ public class FileSystemResource extends AbstractCaosDBServerResource {
       try {
         getEntity(specifier).checkPermission(EntityPermission.RETRIEVE_FILE);
       } catch (EntityDoesNotExistException exception) {
-        // This file in the file system has no corresponding File record.
-        return error(ServerMessages.NOT_PERMITTED, Status.CLIENT_ERROR_FORBIDDEN);
+        // This file in the file system has no corresponding File record. It
+        // shall not be retrieved by anyone.
+        return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN);
       }
 
       final MediaType mt = MediaType.valueOf(FileUtils.getMimeType(file));
diff --git a/src/main/java/caosdb/server/utils/ServerMessages.java b/src/main/java/caosdb/server/utils/ServerMessages.java
index 124962182a114f05f7bfabee377841412594a828..1381f60f5418d70e5530bfd930deb821738e21b1 100644
--- a/src/main/java/caosdb/server/utils/ServerMessages.java
+++ b/src/main/java/caosdb/server/utils/ServerMessages.java
@@ -26,6 +26,9 @@ import caosdb.server.entity.Message.MessageType;
 
 public class ServerMessages {
 
+  public static final Message UNAUTHENTICATED =
+      new Message(MessageType.Error, 401, "Sign up, please!");
+
   public static final Message ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY =
       new Message(MessageType.Info, 10, "This entity has been deleted successfully.");
 
@@ -133,8 +136,6 @@ public class ServerMessages {
           0,
           "Cannot parse value to boolean (either 'true' or 'false, ignoring case).");
 
-  public static final Message NOT_PERMITTED = new Message(MessageType.Error, 0, "Not permitted.");
-
   public static final Message CANNOT_CONNECT_TO_DATABASE =
       new Message(MessageType.Error, 0, "Could not connect to MySQL server.");
 
diff --git a/src/test/java/caosdb/server/authentication/AuthTokenTest.java b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
index 442e7d2196cbac2691b0196b6830c780624f064f..75bbbb56a60b4510ea7496770256de2ee6148087 100644
--- a/src/test/java/caosdb/server/authentication/AuthTokenTest.java
+++ b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
 import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
 import caosdb.server.accessControl.AnonymousAuthenticationToken;
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.OneTimeAuthenticationToken;
@@ -33,6 +34,7 @@ import caosdb.server.accessControl.OneTimeAuthenticationToken.Config;
 import caosdb.server.accessControl.Principal;
 import caosdb.server.accessControl.SelfValidatingAuthenticationToken;
 import caosdb.server.accessControl.SessionToken;
+import caosdb.server.accessControl.SessionTokenRealm;
 import caosdb.server.database.BackendTransaction;
 import caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl;
 import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
@@ -46,9 +48,11 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
+import java.util.List;
 import java.util.Map;
 import org.apache.commons.io.input.CharSequenceInputStream;
 import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.config.Ini;
 import org.apache.shiro.config.IniSecurityManagerFactory;
 import org.apache.shiro.mgt.SecurityManager;
@@ -227,17 +231,121 @@ public class AuthTokenTest {
             60000,
             curry,
             new String[] {"permissions"},
-            new String[] {"roles"});
-    System.err.println(t1.toString());
+            new String[] {"roles"},
+            1L);
+    Assert.assertEquals(1L, t1.getMaxAttempts());
     Assert.assertFalse(t1.isExpired());
     Assert.assertTrue(t1.isHashValid());
     Assert.assertTrue(t1.isValid());
 
+    String serialized = t1.toString();
+    SelfValidatingAuthenticationToken parsed = OneTimeAuthenticationToken.parse(serialized, curry);
+
+    Assert.assertEquals(t1, parsed);
+    Assert.assertEquals(serialized, parsed.toString());
+
+    Assert.assertEquals(1L, t1.getMaxAttempts());
+    Assert.assertFalse(parsed.isExpired());
+    Assert.assertTrue(parsed.isHashValid());
+    Assert.assertTrue(parsed.isValid());
+  }
+
+  @Test(expected = AuthenticationException.class)
+  public void testOneTimeTokenConsume() {
+    final String curry = null;
+    final OneTimeAuthenticationToken t1 =
+        new OneTimeAuthenticationToken(
+            new Principal("somerealm", "someuser"),
+            60000,
+            curry,
+            new String[] {"permissions"},
+            new String[] {"roles"},
+            3L);
+    Assert.assertFalse(t1.isExpired());
+    Assert.assertTrue(t1.isHashValid());
+    Assert.assertTrue(t1.isValid());
+    try {
+      t1.consume();
+      t1.consume();
+      t1.consume();
+    } catch (AuthenticationException e) {
+      Assert.fail(e.getMessage());
+    }
+
+    // throws
+    t1.consume();
+    Assert.fail("4th time consume() should throw");
+  }
+
+  @Test(expected = AuthenticationException.class)
+  public void testOneTimeTokenConsumeByParsing() {
+    final String curry = null;
+    final OneTimeAuthenticationToken t1 =
+        new OneTimeAuthenticationToken(
+            new Principal("somerealm", "someuser"),
+            60000,
+            curry,
+            new String[] {"permissions"},
+            new String[] {"roles"},
+            3L);
+    Assert.assertFalse(t1.isExpired());
+    Assert.assertTrue(t1.isHashValid());
+    Assert.assertTrue(t1.isValid());
+
+    String serialized = t1.toString();
+    try {
+      SelfValidatingAuthenticationToken parsed1 =
+          OneTimeAuthenticationToken.parse(serialized, curry);
+      Assert.assertTrue(parsed1.isValid());
+      SelfValidatingAuthenticationToken parsed2 =
+          OneTimeAuthenticationToken.parse(serialized, curry);
+      Assert.assertTrue(parsed2.isValid());
+      SelfValidatingAuthenticationToken parsed3 =
+          OneTimeAuthenticationToken.parse(serialized, curry);
+      Assert.assertTrue(parsed3.isValid());
+    } catch (AuthenticationException e) {
+      Assert.fail(e.getMessage());
+    }
+
+    // throws
+    OneTimeAuthenticationToken.parse(serialized, curry);
+    Assert.fail("4th parsing should throw");
+  }
+
+  @Test
+  public void testOneTimeTokenConfigEmpty() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("[]");
+
+    List<Config> configs =
+        OneTimeAuthenticationToken.loadConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+
+    Assert.assertTrue("empty config", configs.isEmpty());
+  }
+
+  @Test
+  public void testOneTimeTokenConfigDefaults() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("roles: []\n");
+
+    List<Config> configs =
+        OneTimeAuthenticationToken.loadConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+
+    Assert.assertEquals(1, configs.size());
+    Assert.assertTrue("parsing to Config object", configs.get(0) instanceof Config);
+
+    Config config = configs.get(0);
+
     Assert.assertEquals(
-        t1.toString(), OneTimeAuthenticationToken.parse(t1.toString(), curry).toString());
-    Assert.assertFalse(OneTimeAuthenticationToken.parse(t1.toString(), curry).isExpired());
-    Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isHashValid());
-    Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isValid());
+        Integer.parseInt(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS)),
+        config.getTimeout());
+    Assert.assertEquals(1, config.getMaxAttempts());
+    Assert.assertNull("no purpose", config.getPurpose());
+
+    Assert.assertArrayEquals("no permissions", new String[] {}, config.getPermissions());
+    Assert.assertArrayEquals("no roles", new String[] {}, config.getRoles());
+    Assert.assertNull("no output", config.getOutput());
   }
 
   @Test
@@ -245,6 +353,8 @@ public class AuthTokenTest {
     StringBuilder testYaml = new StringBuilder();
     testYaml.append("purpose: test purpose 1\n");
     testYaml.append("roles: [ role1, \"role2\"]\n");
+    testYaml.append("expiresAfterSeconds: 10\n");
+    testYaml.append("maxAttempts: 3\n");
     testYaml.append("permissions:\n");
     testYaml.append("  - permission1\n");
     testYaml.append("  - 'permission2'\n");
@@ -256,6 +366,8 @@ public class AuthTokenTest {
     Assert.assertNotNull("test purpose there", map.get("test purpose 1"));
     Assert.assertTrue("parsing to Config object", map.get("test purpose 1") instanceof Config);
     Config config = map.get("test purpose 1");
+    Assert.assertEquals(10000, config.getTimeout());
+    Assert.assertEquals(3, config.getMaxAttempts());
 
     Assert.assertArrayEquals(
         "permissions parsed",
@@ -352,4 +464,24 @@ public class AuthTokenTest {
         OneTimeAuthenticationToken.generateForPurpose("for anonymous", anonymous, null);
     assertEquals("anonymous", token.getPrincipal().getUsername());
   }
+
+  @Test
+  public void testSessionTokenRealm() {
+    Config config = new Config();
+    OneTimeAuthenticationToken token = OneTimeAuthenticationToken.generate(config);
+
+    String serialized = token.toString();
+    SelfValidatingAuthenticationToken parsed =
+        SelfValidatingAuthenticationToken.parse(serialized, null);
+
+    SessionTokenRealm sessionTokenRealm = new SessionTokenRealm();
+    Assert.assertTrue(sessionTokenRealm.supports(token));
+    Assert.assertTrue(sessionTokenRealm.supports(parsed));
+
+    Assert.assertNotNull(sessionTokenRealm.getAuthenticationInfo(token));
+    Assert.assertNotNull(sessionTokenRealm.getAuthenticationInfo(parsed));
+
+    Subject anonymous = SecurityUtils.getSubject();
+    anonymous.login(token);
+  }
 }