diff --git a/.gitignore b/.gitignore
index 06c8c148f1e9f45493f574a11c6789398246defd..ab11f8441c1690ede967897c0e64745f9ad30f86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,7 @@ log/
 OUTBOX
 ConsistencyTest.xml
 testlog/
+authtoken/
 
 # python
 __pycache__
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8587b4072e1d7485406cd432aff9034e6c2e6b84..c1afaf3f2a5274ff9e9a3083dbfd788e629709c0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -60,7 +60,6 @@ test:
     - make easy-units
     - mvn antlr4:antlr4
     - mvn compile
-    - echo "defaultRealm = CaosDB" > conf/ext/usersources.ini
     - mvn test
 
 # Deploy: Trigger building of server image and integration tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27382acd3b6ac4e94224186a6e89664470c20cad..22ce0c27bc196c81f1aa10f132b2d62b50f1cc63 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+- One-time Authentication Tokens for login without credentials and login with
+  particular permissions and roles for the course of the session.
 - `Entity/names` resource for retrieving all known entity names.
 - Scripting is simplified by adding a `home` directory, of which a copy is
   created for each called script and set as the `HOME` environment variable.
@@ -30,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Deprecated
 
-- 
+- CaosDBTerminal
 
 ### Removed
 
diff --git a/README_CONFIGURATION.md b/README_CONFIGURATION.md
index 55b47175762f15266e66bcca8d3b17143baaf578..27045eea4523c5009e246fe506ec6319965d8e9f 100644
--- a/README_CONFIGURATION.md
+++ b/README_CONFIGURATION.md
@@ -13,3 +13,9 @@ The default configuration can be overriden by
 
 in this order.
 
+# One-time Authentication Tokens
+
+One-time Authentication Tokens can be configure to be issued for special purposes (e.g. a call of a server-side script) or to be written to a file on a regular basis.
+
+An example of a configuration is located at `./conf/core/authtoken.example.yaml`.
+
diff --git a/conf/core/authtoken.example.yaml b/conf/core/authtoken.example.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..453630a852642db19cfc824bf660b004e26af5b2
--- /dev/null
+++ b/conf/core/authtoken.example.yaml
@@ -0,0 +1,94 @@
+# OneTimeAuthenticationToken Config
+#
+# One-Time Authentication (OTA) Tokens are a means to authenticate a client
+# without a password.
+#
+# This example config file illustrates several use cases of One-Time
+# Authentication Tokens.
+#
+# This yaml file contains an array of configuration objects which may have the
+# properties that are defined by the caosdb.server.accessControl.Config class.
+#
+# These properties are:
+#
+# - expiresAfter:: An integer timespan in milliseconds after the OTA Token was
+#     generated by the server when the token expires. So the token will not be
+#     valid after <creationDate> + <expiresAfter>.
+# - expiresAfterSeconds:: A convenient option which has the same meaning as
+#     expiresAfter but measured in seconds instead of milliseconds.
+# - roles:: A list of strings which are the user roles that the client has when
+#     it authenticates with this token. This is a way to give a client a set of
+#     roles which it wouldn't have otherwise. Consequently, the client also has
+#     all permissions of its roles.
+# - permissions:: A list of string which are the permissions that the client has
+#     when it authenticates with this token. This is another way to give a
+#     client permissions which it wouldn't have otherwise.
+# - maxReplays:: Normally a One-Time token can be used exactly once and
+#     invalidates the moment when the server recognizes that a client uses it.
+#     However, when a network connection is unreliable, it should be possible to
+#     attempt another login with the same token for a few times. And in another
+#     use case, it might be practical to authenticate several clients at the
+#     same time with the same authentication token. The maxReplays value is the
+#     number of times that a server accepts an OTA token as valid. The default
+#     is, of course, 1.
+# - replayTimeout:: An integer timespan in milliseconds. Because replays are a
+#     possible attack vector, the time span in which the same token may be used
+#     is limited by this value. The default value is configured by the server
+#     property 'ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS' which has a default of 30000,
+#     which is 30 seconds.
+# - replayTimeoutSeconds:: A convenient option which has the same meaning as
+#     replayTimeout but measured in seconds instead of milliseconds.
+# - output:: Defines how the OTA token is output by the server. If this property
+#     is not present the OTA is not output in any way but only used internally
+#     (see purpose). The 'output' object has the following properties:
+#      - file:: An absolute or relative path in the server's file system. This
+#        property means that the OTA is written to that file on server start.
+#      - schedule:: A string formatted according to the Quartz[1] library
+#        indicating when the OTA is renewed and the file is being overridden
+#        with the new OTA.
+# - purpose:: A string which is used (only internally so far) to generate an OTA
+#     for a special purpose, e.g. the execution of a server-side script with
+#     particular permissions. This way, an otherwise unprivileged client may
+#     execute a server-side script with all necessary permissions, if the client
+#     has the "SCRIPTING:EXECUTE:<script-path>" permission and this config file
+#     has a configuration with "purpose: SCRIPTING:EXECUTE:<script-path>"
+#     (case-sensitive).
+#
+# [1] http://www.quartz-scheduler.org/api/2.3.0/org/quartz/CronExpression.html
+#
+#
+# Examples:
+#
+# 1. Every client with the SCRIPTING:EXECUTE:administration/diagnostics.py
+#    permission can execute the administration/diagnostics.py script with the
+#    administration role (and not the client's roles).
+- purpose: SCRIPTING:EXECUTE:administration/diagnostics.py
+  roles:
+    - administration
+# 2. The server writes an OTA token with the administration role to
+#    "authtoken/admin_token_crud.txt" and refreshes that token every 10 seconds.
+- roles:
+    - administration
+  output:
+    file: "authtoken/admin_token_crud.txt"
+    schedule: "0/10 * * ? * * *"
+
+# 3. The server writes an OTA token with the administration role to
+#    "authtoken/admin_token_3_attempts.txt" which can be replayed 3 times in 10
+#    seconds. The OTA token is refreshed every 10 seconds.
+- roles:
+    - administration
+  output:
+    file: "authtoken/admin_token_3_attempts.txt"
+    schedule: "0/10 * * ? * * *"
+  maxReplays: 3
+  replayTimeout: 10000
+
+# 4. The server writes an OTA token with the administration role to
+#    "authtoken/admin_token_expired.txt" which expires immediately. Of course
+#    this is only useful for testing.
+- roles:
+    - administration
+  output:
+    file: "authtoken/admin_token_expired.txt"
+  expiresAfterSeconds: 0
diff --git a/conf/core/server.conf b/conf/core/server.conf
index 358f6b5c14106b87d7a676e2262474772ae98512..d30d82b681a185a820783c6c2d8d81a62981eb64 100644
--- a/conf/core/server.conf
+++ b/conf/core/server.conf
@@ -114,10 +114,21 @@ CERTIFICATES_KEY_STORE_PASSWORD=
 # 10 min
 SESSION_TIMEOUT_MS=600000
 
-# Time after which activation tokens for the activation of new users (internal
-# user sources) expire.
+# Time after which one-time tokens expire.
+# This is only a default value. The actual timeout of tokens can be
+# configured otherwise, for example in authtoken.yml.
 # 7days
-ACTIVATION_TIMEOUT_MS=604800000
+ONE_TIME_TOKEN_EXPIRES_MS=604800000
+
+# Path to config file for one time tokens, for example authtoken.yml.
+AUTHTOKEN_CONFIG=
+
+# Timeout after which a one-time token expires once it has been first consumed,
+# regardless of the maximum of replays that are allowed for that token. This is
+# only a default value. The actual timeout of tokens can be configured
+# otherwise.
+# 30 s
+ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS=30000
 
 # The value for the HTTP cache directive "max-age"
 WEBUI_HTTP_HEADER_CACHE_MAX_AGE=28800
diff --git a/makefile b/makefile
index 55631222cbb0613e6ccd634a609b594a50ff0951..e900742bededb1651c9e64963efe6db01014903e 100644
--- a/makefile
+++ b/makefile
@@ -47,6 +47,7 @@ run-single:
 
 formatting:
 	mvn fmt:format
+	autopep8 -ari scripting/
 
 # Compile into a standalone jar file
 jar: easy-units
diff --git a/pom.xml b/pom.xml
index c8f79b3dcc43759cc941244283d13b248d811bb8..0594c0548331f0e834fc0bc10a61fec69ffa9a69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,10 +54,20 @@
       <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>
+      <version>2.11.0</version>
+    </dependency>
     <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-core</artifactId>
-      <version>1.4.1</version>
+      <version>1.5.3</version>
     </dependency>
     <dependency>
       <groupId>junit</groupId>
diff --git a/scripting/bin/administration/diagnostics.py b/scripting/bin/administration/diagnostics.py
new file mode 100755
index 0000000000000000000000000000000000000000..ccecbd583ffdaf5f941a2df873ac10982560af31
--- /dev/null
+++ b/scripting/bin/administration/diagnostics.py
@@ -0,0 +1,149 @@
+#!/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",
+    "validate_email"
+]
+
+
+def get_files():
+    from os import walk
+    from os.path import join
+    result = []
+    for p, dirs, files in walk("."):
+        for f in files:
+            result.append(join(p, f))
+        for d in dirs:
+            result.append(join(p, d))
+    return result
+
+
+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()
+    result["version"] = db.version.version
+
+    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)
+            if hasattr(i, "__version__") and i.__version__ is not None:
+                v = i.__version__
+            else:
+                v = "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)
+        diagnostics["files"] = get_files()
+
+        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/scripting/bin/xls_from_csv.py b/scripting/bin/xls_from_csv.py
index 38dfbc1c9392ce60d338b102f9b9fd05309919d3..73bb6b0ac23fc2df093bd6797e864aad1f2d5592 100755
--- a/scripting/bin/xls_from_csv.py
+++ b/scripting/bin/xls_from_csv.py
@@ -93,7 +93,8 @@ def _parse_arguments():
     parser.add_argument('-t', '--tempdir', required=False, default=tempdir,
                         help="Temporary dir for saving the result.")
     parser.add_argument('-a', '--auth-token', required=False,
-                        help="An authentication token (not needed, only for compatibility).")
+                        help=("An authentication token (not needed, only for "
+                              "compatibility)."))
     parser.add_argument('tsv', help="The tsv file.")
     return parser.parse_args()
 
@@ -104,5 +105,6 @@ def main():
     filename = _write_xls(dataframe, directory=args.tempdir)
     print(filename)
 
+
 if __name__ == "__main__":
     main()
diff --git a/src/main/java/caosdb/server/CaosAuthenticator.java b/src/main/java/caosdb/server/CaosAuthenticator.java
index 25f112aa52050a0b872b2ff83e95400ad4bd925e..8fefee3b453dabc0c613228e2495acacb19deaa0 100644
--- a/src/main/java/caosdb/server/CaosAuthenticator.java
+++ b/src/main/java/caosdb/server/CaosAuthenticator.java
@@ -22,12 +22,13 @@
  */
 package caosdb.server;
 
+import caosdb.server.accessControl.AnonymousAuthenticationToken;
 import caosdb.server.accessControl.AuthenticationUtils;
-import caosdb.server.accessControl.OneTimeAuthenticationToken;
-import caosdb.server.accessControl.SessionToken;
 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;
 import org.apache.shiro.subject.Subject;
 import org.restlet.Context;
 import org.restlet.Request;
@@ -48,50 +49,26 @@ public class CaosAuthenticator extends Authenticator {
   protected boolean authenticate(final Request request, final Response response) {
     final Subject subject = SecurityUtils.getSubject();
 
-    return attemptOneTimeTokenLogin(subject, request) || attemptSessionValidation(subject, request);
+    return attemptSessionValidation(subject, request);
   }
 
   private static boolean attemptSessionValidation(final Subject subject, final Request request) {
     try {
-      final SessionToken sessionToken =
+      final AuthenticationToken sessionToken =
           AuthenticationUtils.parseSessionTokenCookie(
-              request.getCookies().getFirst(AuthenticationUtils.SESSION_TOKEN_COOKIE), null);
+              request.getCookies().getFirst(AuthenticationUtils.SESSION_TOKEN_COOKIE));
 
       if (sessionToken != null) {
         subject.login(sessionToken);
       }
-    } catch (AuthenticationException e) {
-      logger.info("LOGIN_FAILED", e);
-    }
-    // anonymous users
-    if (!subject.isAuthenticated()
-        && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
-            .equalsIgnoreCase("TRUE")) {
-      subject.login(AuthenticationUtils.ANONYMOUS_USER);
-    }
-    return subject.isAuthenticated();
-  }
 
-  private static boolean attemptOneTimeTokenLogin(final Subject subject, final Request request) {
-    try {
-      OneTimeAuthenticationToken oneTimeToken = null;
-
-      // try and parse from the query segment of the uri
-      oneTimeToken =
-          AuthenticationUtils.parseOneTimeTokenQuerySegment(
-              request.getResourceRef().getQueryAsForm().getFirstValue("AuthToken"), null);
-
-      // try and parse from cookie
-      if (oneTimeToken == null) {
-        oneTimeToken =
-            AuthenticationUtils.parseOneTimeTokenCookie(
-                request.getCookies().getFirst(AuthenticationUtils.ONE_TIME_TOKEN_COOKIE), null);
+      // anonymous users
+      if (!subject.isAuthenticated()
+          && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
+              .equalsIgnoreCase("TRUE")) {
+        subject.login(AnonymousAuthenticationToken.getInstance());
       }
-
-      if (oneTimeToken != null) {
-        subject.login(oneTimeToken);
-      }
-    } catch (final AuthenticationException e) {
+    } catch (AuthenticationException e) {
       logger.info("LOGIN_FAILED", e);
     }
     return subject.isAuthenticated();
@@ -100,7 +77,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 34bfe3e2911955fa724ea5e6ac7258d80f9cc120..45da4eb0e655f011e06deede49eec2035745fac1 100644
--- a/src/main/java/caosdb/server/CaosDBServer.java
+++ b/src/main/java/caosdb/server/CaosDBServer.java
@@ -19,12 +19,13 @@
  */
 package caosdb.server;
 
+import caosdb.server.accessControl.AnonymousAuthenticationToken;
 import caosdb.server.accessControl.AnonymousRealm;
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.CaosDBAuthorizingRealm;
 import caosdb.server.accessControl.CaosDBDefaultRealm;
-import caosdb.server.accessControl.OneTimeTokenRealm;
-import caosdb.server.accessControl.Principal;
+import caosdb.server.accessControl.ConsumedInfoCleanupJob;
+import caosdb.server.accessControl.OneTimeAuthenticationToken;
 import caosdb.server.accessControl.SessionToken;
 import caosdb.server.accessControl.SessionTokenRealm;
 import caosdb.server.database.BackendTransaction;
@@ -80,11 +81,15 @@ import java.util.logging.LogRecord;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.config.Ini;
 import org.apache.shiro.config.Ini.Section;
-import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.env.BasicIniEnvironment;
 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;
@@ -118,11 +123,68 @@ public class CaosDBServer extends Application {
   private static ArrayList<Runnable> preShutdownHooks = new ArrayList<Runnable>();
   private static boolean START_BACKEND = true;
   private static boolean INSECURE = false;
+  public static final String REQUEST_TIME_LOGGER = "REQUEST_TIME_LOGGER";
+  public static final String REQUEST_ERRORS_LOGGER = "REQUEST_ERRORS_LOGGER";
+  private static Scheduler SCHEDULER;
 
   public static String getServerProperty(final String key) {
     return getServerProperties().getProperty(key);
   }
 
+  /**
+   * This main method starts up a web application that will listen on a port defined in the config
+   * file.
+   *
+   * @param args One option temporarily (for testing) available: silent: If present: disable
+   *     System.out-stream (stream to a NullPrintStream). This makes the response of the database
+   *     amazingly faster.
+   * @throws IOException
+   * @throws FileNotFoundException
+   * @throws SecurityException
+   * @throws Exception If problems occur.
+   */
+  public static void main(final String[] args)
+      throws SecurityException, FileNotFoundException, IOException {
+    try {
+      init(args);
+      initScheduler();
+      initServerProperties();
+      initTimeZone();
+      initOneTimeTokens();
+      initShiro();
+      initBackend();
+      initWebServer();
+      initShutDownHook();
+    } catch (Exception e1) {
+      logger.error("Could not start the server.", e1);
+      System.exit(1);
+    }
+  }
+
+  private static void init(final String[] args) {
+    // Important change:
+    // Make silent the default option
+    START_GUI = false;
+    for (final String s : args) {
+      if (s.equals("silent")) {
+        START_GUI = false;
+      } else if (s.equals("gui")) {
+        START_GUI = true;
+      } else if (s.equals("nobackend")) {
+        START_BACKEND = false;
+      } else if (s.equals("insecure")) {
+        INSECURE = true;
+      }
+    }
+    INSECURE = INSECURE && isDebugMode(); // only allow insecure in debug mode
+    START_BACKEND = START_BACKEND || !isDebugMode(); // always start backend if not in debug mode
+  }
+
+  private static void initScheduler() throws SchedulerException {
+    SCHEDULER = StdSchedulerFactory.getDefaultScheduler();
+    SCHEDULER.start();
+  }
+
   public static void initServerProperties() throws IOException {
     SERVER_PROPERTIES = ServerProperties.initServerProperties();
   }
@@ -206,21 +268,15 @@ public class CaosDBServer extends Application {
     }
   }
 
-  private static void init(final String[] args) {
-    // Important change:
-    // Make silent the default option
-    START_GUI = false;
-    for (final String s : args) {
-      if (s.equals("silent")) {
-        START_GUI = false;
-      } else if (s.equals("gui")) {
-        START_GUI = true;
-      } else if (s.equals("nobackend")) {
-        START_BACKEND = false;
-      } else if (s.equals("insecure")) {
-        INSECURE = true;
-      }
-    }
+  public static void initOneTimeTokens() throws Exception {
+    OneTimeAuthenticationToken.initConfig();
+    ConsumedInfoCleanupJob.scheduleDaily();
+  }
+
+  public static void initShiro() {
+    // init Shiro (user authentication/authorization and session management)
+    final Ini config = getShiroConfig();
+    initShiro(config);
   }
 
   public static Ini getShiroConfig() {
@@ -228,12 +284,11 @@ public class CaosDBServer extends Application {
     final Section mainSec = config.addSection("main");
     mainSec.put("CaosDB", CaosDBDefaultRealm.class.getCanonicalName());
     mainSec.put("SessionTokenValidator", SessionTokenRealm.class.getCanonicalName());
-    mainSec.put("OneTimeTokenValidator", OneTimeTokenRealm.class.getCanonicalName());
     mainSec.put("CaosDBAuthorizingRealm", CaosDBAuthorizingRealm.class.getCanonicalName());
     mainSec.put("AnonymousRealm", AnonymousRealm.class.getCanonicalName());
     mainSec.put(
         "securityManager.realms",
-        "$CaosDB, $SessionTokenValidator, $OneTimeTokenValidator, $CaosDBAuthorizingRealm, $AnonymousRealm");
+        "$CaosDB, $SessionTokenValidator, $CaosDBAuthorizingRealm, $AnonymousRealm");
 
     // disable shiro's default session management. We have quasi-stateless
     // sessions
@@ -243,43 +298,15 @@ public class CaosDBServer extends Application {
     return config;
   }
 
-  /**
-   * This main method starts up a web application that will listen on a port defined in the config
-   * file.
-   *
-   * @param args One option temporarily (for testing) available: silent: If present: disable
-   *     System.out-stream (stream to a NullPrintStream). This makes the response of the database
-   *     amazingly faster.
-   * @throws IOException
-   * @throws FileNotFoundException
-   * @throws SecurityException
-   * @throws Exception If problems occur.
-   */
-  public static void main(final String[] args)
-      throws SecurityException, FileNotFoundException, IOException {
-    try {
-      init(args);
-      initServerProperties();
-      initTimeZone();
-    } catch (IOException | InterruptedException e1) {
-      logger.error("Could not configure the server.", e1);
-      System.exit(1);
-    }
-
-    INSECURE = INSECURE && isDebugMode(); // only allow insecure in debug mode
-    START_BACKEND = START_BACKEND || !isDebugMode(); // always start backend if
-    // not in debug mode
-
-    // init Shiro (user authentication/authorization and session management)
-    final Ini config = getShiroConfig();
-    final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
-    final SecurityManager securityManager = factory.getInstance();
+  public static void initShiro(Ini config) {
+    BasicIniEnvironment env = new BasicIniEnvironment(config);
+    final SecurityManager securityManager = env.getSecurityManager();
     SecurityUtils.setSecurityManager(securityManager);
+  }
 
-    final Initialization init = Initialization.setUp();
-    try {
-      // init backend
-      if (START_BACKEND) {
+  public static void initBackend() throws Exception {
+    if (START_BACKEND) {
+      try (final Initialization init = Initialization.setUp()) {
         BackendTransaction.init();
 
         // init benchmark
@@ -296,65 +323,60 @@ public class CaosDBServer extends Application {
 
         // ChecksumUpdater
         ChecksumUpdater.start();
-      } else {
-        logger.info("NO BACKEND");
       }
+    } else {
+      logger.info("NO BACKEND");
+    }
+  }
 
-      // GUI
-      if (START_GUI) {
-        final CaosDBTerminal caosDBTerminal = new CaosDBTerminal();
-        caosDBTerminal.setName("CaosDBTerminal");
-        caosDBTerminal.start();
+  private static void initWebServer() throws Exception {
+    final int port_https =
+        Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTPS));
+    final int port_http =
+        Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTP));
+    int port_redirect_https;
+    try {
+      port_redirect_https =
+          Integer.parseInt(getServerProperty(ServerProperties.KEY_REDIRECT_HTTP_TO_HTTPS_PORT));
+    } catch (NumberFormatException e) {
+      port_redirect_https = port_https;
+    }
+    final int initialConnections =
+        Integer.parseInt(getServerProperty(ServerProperties.KEY_INITIAL_CONNECTIONS));
+    final int maxTotalConnections =
+        Integer.parseInt(getServerProperty(ServerProperties.KEY_MAX_CONNECTIONS));
 
-        addPreShutdownHook(
-            new Runnable() {
+    if (INSECURE) {
+      runHTTPServer(port_http, initialConnections, maxTotalConnections);
+    } else {
+      runHTTPSServer(
+          port_https, port_http, port_redirect_https, initialConnections, maxTotalConnections);
+    }
+  }
 
-              @Override
-              public void run() {
-                caosDBTerminal.shutDown();
-                SystemErrPanel.close();
-              }
-            });
-        // wait until the terminal is initialized.
-        Thread.sleep(1000);
-
-        // add Benchmark
-        StatsPanel.addStat("TransactionBenchmark", TransactionBenchmark.getRootInstance());
-      } else {
-        logger.info("NO GUI");
-        System.setOut(new NullPrintStream());
-      }
+  public static void initGUI() throws InterruptedException {
+    if (START_GUI) {
+      final CaosDBTerminal caosDBTerminal = new CaosDBTerminal();
+      caosDBTerminal.setName("CaosDBTerminal");
+      caosDBTerminal.start();
 
-      // Web server properties
-      final int port_https =
-          Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTPS));
-      final int port_http =
-          Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTP));
-      int port_redirect_https;
-      try {
-        port_redirect_https =
-            Integer.parseInt(getServerProperty(ServerProperties.KEY_REDIRECT_HTTP_TO_HTTPS_PORT));
-      } catch (NumberFormatException e) {
-        port_redirect_https = port_https;
-      }
-      final int initialConnections =
-          Integer.parseInt(getServerProperty(ServerProperties.KEY_INITIAL_CONNECTIONS));
-      final int maxTotalConnections =
-          Integer.parseInt(getServerProperty(ServerProperties.KEY_MAX_CONNECTIONS));
-
-      init.release();
-
-      if (INSECURE) {
-        runHTTPServer(port_http, initialConnections, maxTotalConnections);
-      } else {
-        runHTTPSServer(
-            port_https, port_http, port_redirect_https, initialConnections, maxTotalConnections);
-      }
-      initShutDownHook();
-    } catch (final Exception e) {
-      logger.error("Server start failed.", e);
-      init.release();
-      System.exit(1);
+      addPreShutdownHook(
+          new Runnable() {
+
+            @Override
+            public void run() {
+              caosDBTerminal.shutDown();
+              SystemErrPanel.close();
+            }
+          });
+      // wait until the terminal is initialized.
+      Thread.sleep(1000);
+
+      // add Benchmark
+      StatsPanel.addStat("TransactionBenchmark", TransactionBenchmark.getRootInstance());
+    } else {
+      logger.info("NO GUI");
+      System.setOut(new NullPrintStream());
     }
   }
 
@@ -508,9 +530,6 @@ 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";
-
   /**
    * Specify the dispatching restlet that maps URIs to their associated resources for processing.
    *
@@ -560,10 +579,10 @@ public class CaosDBServer extends Application {
           private void setSessionCookies(final Response response) {
 
             final Subject subject = SecurityUtils.getSubject();
+            // if authenticated as a normal user: generate and set session cookie.
             if (subject.isAuthenticated()
-                && subject.getPrincipal() != AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) {
-              final SessionToken sessionToken =
-                  SessionToken.generate((Principal) subject.getPrincipal(), null);
+                && subject.getPrincipal() != AnonymousAuthenticationToken.PRINCIPAL) {
+              final SessionToken sessionToken = SessionToken.generate(subject);
 
               // set session token cookie (httpOnly, secure cookie which
               // is used to recognize a user session)
@@ -823,6 +842,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/ServerProperties.java b/src/main/java/caosdb/server/ServerProperties.java
index c1dd59f3902601ea90c021a4a7b723723d1b0670..29ba470ddbcad0e79db210917234562c4b58620a 100644
--- a/src/main/java/caosdb/server/ServerProperties.java
+++ b/src/main/java/caosdb/server/ServerProperties.java
@@ -88,7 +88,9 @@ public class ServerProperties extends Properties {
   public static final String KEY_BUGTRACKER_URI = "BUGTRACKER_URI";
 
   public static final String KEY_SESSION_TIMEOUT_MS = "SESSION_TIMEOUT_MS";
-  public static final String KEY_ACTIVATION_TIMEOUT_MS = "ACTIVATION_TIMEOUT_MS";
+  public static final String KEY_ONE_TIME_TOKEN_EXPIRES_MS = "ONE_TIME_TOKEN_EXPIRES_MS";
+  public static final String KEY_ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS =
+      "ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS";
 
   public static final String KEY_CACHE_CONF_LOC = "CACHE_CONF_LOC";
   public static final String KEY_CACHE_DISABLE = "CACHE_DISABLE";
@@ -129,6 +131,7 @@ public class ServerProperties extends Properties {
   public static final String KEY_TIMEZONE = "TIMEZONE";
   public static final String KEY_WEBUI_HTTP_HEADER_CACHE_MAX_AGE =
       "WEBUI_HTTP_HEADER_CACHE_MAX_AGE";
+  public static final String KEY_AUTHTOKEN_CONFIG = "AUTHTOKEN_CONFIG";
 
   /**
    * Read the config files and initialize the server properties.
diff --git a/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java
index cd3f86f61eb66759b3eb7d0c91c29dc23637000b..f3f62af2af319d342977a7fe2524b353f8e7adab 100644
--- a/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java
@@ -28,7 +28,7 @@ public class AnonymousAuthenticationToken implements AuthenticationToken {
 
   private static final long serialVersionUID = 1424325396819592888L;
   private static final AnonymousAuthenticationToken INSTANCE = new AnonymousAuthenticationToken();
-  public static final Object PRINCIPAL = new Object();
+  public static final Principal PRINCIPAL = new Principal("anonymous", "anonymous");
 
   private AnonymousAuthenticationToken() {}
 
@@ -37,7 +37,7 @@ public class AnonymousAuthenticationToken implements AuthenticationToken {
   }
 
   @Override
-  public Object getPrincipal() {
+  public Principal getPrincipal() {
     return PRINCIPAL;
   }
 
diff --git a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
index 6e0fd5370ffcc2435067d68b3f2f810819ae9fbb..c3576031da2d9a598bd74a07fe12df62ed7de90d 100644
--- a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
+++ b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -26,19 +28,15 @@ 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;
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.LinkedList;
-import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.subject.Subject;
 import org.restlet.data.Cookie;
 import org.restlet.data.CookieSetting;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Useful static methods, mainly for parsing and serializing SessionTokens by the means of web
@@ -48,16 +46,16 @@ import org.slf4j.LoggerFactory;
  */
 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";
 
-  public static final AuthenticationToken ANONYMOUS_USER =
-      AnonymousAuthenticationToken.getInstance();
+  public static boolean isAnonymous(Subject user) {
+    return AnonymousAuthenticationToken.PRINCIPAL.equals(user.getPrincipal());
+  }
+
+  public static boolean isAnonymous(Principal principal) {
+    return AnonymousAuthenticationToken.PRINCIPAL.equals(principal);
+  }
 
   /**
    * Create a cookie for a {@link SelfValidatingAuthenticationToken}. Returns null if the parameter
@@ -87,11 +85,8 @@ public class AuthenticationUtils {
     return null;
   }
 
-  public static CookieSetting createOneTimeTokenCookie(final OneTimeAuthenticationToken token) {
-    return createTokenCookie(AuthenticationUtils.ONE_TIME_TOKEN_COOKIE, token);
-  }
-
-  public static CookieSetting createSessionTokenCookie(final SessionToken token) {
+  public static CookieSetting createSessionTokenCookie(
+      final SelfValidatingAuthenticationToken token) {
     return createTokenCookie(AuthenticationUtils.SESSION_TOKEN_COOKIE, token);
   }
 
@@ -103,48 +98,16 @@ public class AuthenticationUtils {
    * @return A new SessionToken
    * @see {@link AuthenticationUtils#createSessionTokenCookie(SessionToken)}, {@link SessionToken}
    */
-  public static SessionToken parseSessionTokenCookie(final Cookie cookie, final String curry) {
+  public static SelfValidatingAuthenticationToken parseSessionTokenCookie(final Cookie cookie) {
     if (cookie != null) {
       final String tokenString = URLDecodeWithUTF8(cookie.getValue());
       if (tokenString != null && !tokenString.equals("")) {
-        try {
-          return SessionToken.parse(tokenString, curry);
-        } catch (final Exception e) {
-          logger.warn("AUTHTOKEN_PARSING_FAILED", e);
-        }
+        return SelfValidatingAuthenticationToken.parse(tokenString);
       }
     }
     return null;
   }
 
-  private static OneTimeAuthenticationToken parseOneTimeToken(
-      final String tokenString, final String curry) {
-    if (tokenString != null && !tokenString.equals("")) {
-      try {
-        return OneTimeAuthenticationToken.parse(tokenString, curry);
-      } catch (final Exception e) {
-        logger.warn("AUTHTOKEN_PARSING_FAILED", e);
-      }
-    }
-    return null;
-  }
-
-  public static OneTimeAuthenticationToken parseOneTimeTokenQuerySegment(
-      final String tokenString, final String curry) {
-    if (tokenString != null) {
-      return parseOneTimeToken(URLDecodeWithUTF8(tokenString), curry);
-    }
-    return null;
-  }
-
-  public static OneTimeAuthenticationToken parseOneTimeTokenCookie(
-      final Cookie cookie, final String curry) {
-    if (cookie != null) {
-      return parseOneTimeToken(URLDecodeWithUTF8(cookie.getValue()), curry);
-    }
-    return null;
-  }
-
   /**
    * Create a session timeout cookie. The value is a plain UTC timestamp which tells the user how
    * long his session will stay active. This cookie will be ignored by the server and carries only
@@ -163,9 +126,7 @@ public class AuthenticationUtils {
 
     if (token != null && token.isValid()) {
       t = new Timestamp(token.getExpires()).toString().replaceFirst(" ", "T");
-      exp_in_sec = (int) Math.ceil(token.getTimeout() / 1000.0); // new
-      // expiration
-      // time.
+      exp_in_sec = (int) Math.ceil(token.getTimeout() / 1000.0); // new expiration time
       return new CookieSetting(
           0,
           AuthenticationUtils.SESSION_TIMEOUT_COOKIE,
@@ -180,6 +141,7 @@ public class AuthenticationUtils {
     return null;
   }
 
+  // TODO move
   public static boolean isResponsibleAgentExistent(final ResponsibleAgent agent) {
     // 1) check OWNER, OTHER
     if (Role.OTHER_ROLE.equals(agent) || Role.OWNER_ROLE.equals(agent)) {
@@ -227,4 +189,14 @@ public class AuthenticationUtils {
         false,
         false);
   }
+
+  public static Collection<String> getRoles(Subject user) {
+    return new CaosDBAuthorizingRealm().doGetAuthorizationInfo(user.getPrincipals()).getRoles();
+  }
+
+  public static boolean isFromOneTimeTokenRealm(Subject subject) {
+    return ((Principal) subject.getPrincipal())
+        .getRealm()
+        .equals(OneTimeAuthenticationToken.REALM_NAME);
+  }
 }
diff --git a/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java b/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java
index 5cfa425ac235405a0c861e54c9d97ae8ffab58f5..abd52d9ed306f43c51d8bf52bdf1c25d86d8454e 100644
--- a/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java
+++ b/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java
@@ -22,9 +22,7 @@
  */
 package caosdb.server.accessControl;
 
-import com.google.common.base.Objects;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Collection;
 import java.util.Set;
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
@@ -32,100 +30,47 @@ import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.authz.SimpleAuthorizationInfo;
 import org.apache.shiro.realm.AuthorizingRealm;
 import org.apache.shiro.subject.PrincipalCollection;
-import org.apache.shiro.subject.SimplePrincipalCollection;
 
 public class CaosDBAuthorizingRealm extends AuthorizingRealm {
 
-  private static class PermissionPrincipalCollection extends SimplePrincipalCollection {
-
-    private final List<String> permissions;
-
-    public PermissionPrincipalCollection(
-        final PrincipalCollection principals, final String[] permissions) {
-      super(principals);
-      this.permissions = Arrays.asList(permissions);
-    }
-
-    private static final long serialVersionUID = 5585425107072564933L;
-
-    @Override
-    public boolean equals(final Object obj) {
-      if (obj == this) {
-        return true;
-      } else if (obj instanceof PermissionPrincipalCollection) {
-        final PermissionPrincipalCollection that = (PermissionPrincipalCollection) obj;
-        return Objects.equal(that.permissions, this.permissions) && super.equals(that);
-      } else {
-        return false;
-      }
-    }
+  private static final CaosDBRolePermissionResolver role_permission_resolver =
+      new CaosDBRolePermissionResolver();
 
-    @Override
-    public int hashCode() {
-      return super.hashCode() + 28 * this.permissions.hashCode();
-    }
+  public Collection<String> getSessionRoles(SelfValidatingAuthenticationToken token) {
+    return token.getRoles();
   }
 
-  static class PermissionAuthenticationInfo implements AuthenticationInfo {
-
-    private static final long serialVersionUID = -3714484164124767976L;
-    private final PermissionPrincipalCollection principalCollection;
-
-    public PermissionAuthenticationInfo(
-        final PrincipalCollection principals, final String... permissions) {
-      this.principalCollection = new PermissionPrincipalCollection(principals, permissions);
-    }
+  public Collection<String> getSessionPermissions(SelfValidatingAuthenticationToken token) {
+    return token.getPermissions();
+  }
 
-    @Override
-    public PrincipalCollection getPrincipals() {
-      return this.principalCollection;
-    }
+  @Override
+  protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
+    final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
+    Object principal = principals.getPrimaryPrincipal();
 
-    @Override
-    public Object getCredentials() {
-      return null;
-    }
+    // Add explicitly given roles and permissions.
+    if (principal instanceof SelfValidatingAuthenticationToken) {
+      Collection<String> sessionPermissions =
+          getSessionPermissions((SelfValidatingAuthenticationToken) principal);
 
-    @Override
-    public boolean equals(final Object obj) {
-      if (obj == this) {
-        return true;
-      } else if (obj instanceof PermissionAuthenticationInfo) {
-        final PermissionAuthenticationInfo that = (PermissionAuthenticationInfo) obj;
-        return Objects.equal(that.principalCollection, this.principalCollection);
-      } else {
-        return false;
-      }
-    }
+      Collection<String> sessionRoles =
+          getSessionRoles((SelfValidatingAuthenticationToken) principal);
 
-    @Override
-    public int hashCode() {
-      return this.principalCollection.hashCode();
+      authzInfo.addRoles(sessionRoles);
+      authzInfo.addStringPermissions(sessionPermissions);
     }
-  }
-
-  private static final CaosDBRolePermissionResolver role_permission_resolver =
-      new CaosDBRolePermissionResolver();
 
-  @Override
-  protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
-    if (principals instanceof PermissionPrincipalCollection) {
-      // the PrincialsCollection carries the permissions.
-      final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
-      info.addStringPermissions(((PermissionPrincipalCollection) principals).permissions);
-      return info;
+    // Find all roles which are associated with this principal in this realm.
+    final Set<String> principalRoles =
+        UserSources.resolve((Principal) principals.getPrimaryPrincipal());
+    if (principalRoles != null) {
+      authzInfo.addRoles(principalRoles);
     }
 
-    final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
-
-    // find all roles which are associated with this principal in this
-    // realm.
-    final Set<String> roles = UserSources.resolve(principals);
-    if (roles != null) {
-      authzInfo.setRoles(roles);
-
-      // find all permissions which are associated with these roles.
-      authzInfo.addObjectPermission(role_permission_resolver.resolvePermissionsInRole(roles));
+    if (authzInfo.getRoles() != null && !authzInfo.getRoles().isEmpty()) {
+      authzInfo.addObjectPermission(
+          role_permission_resolver.resolvePermissionsInRole(authzInfo.getRoles()));
     }
 
     return authzInfo;
diff --git a/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java b/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
index 3350a68700f108e8ed5cf7309e9f305ac0bb669b..ae4c38605f07f5222499e076def3a3d48c31a385 100644
--- a/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
+++ b/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
@@ -33,6 +33,7 @@ import org.apache.shiro.authc.AuthenticationException;
 
 public class CaosDBRolePermissionResolver {
 
+  /** Return CaosPermission with the rules which are associated with the roles. */
   public CaosPermission resolvePermissionsInRole(final Set<String> roles) {
     final HashSet<PermissionRule> rules = new HashSet<PermissionRule>();
     for (final String role : roles) {
diff --git a/src/main/java/caosdb/server/accessControl/Config.java b/src/main/java/caosdb/server/accessControl/Config.java
new file mode 100644
index 0000000000000000000000000000000000000000..3da7a426a9dcecfb93264cb9e504a157b10659c8
--- /dev/null
+++ b/src/main/java/caosdb/server/accessControl/Config.java
@@ -0,0 +1,86 @@
+package caosdb.server.accessControl;
+
+public class Config {
+  private String[] permissions = {};
+  private String[] roles = {};
+  private String purpose = null;
+  private OneTimeTokenToFile output = null;
+  private int maxReplays = 1;
+  private long expiresAfter = OneTimeAuthenticationToken.DEFAULT_TIMEOUT_MS;
+  private long replayTimeout = OneTimeAuthenticationToken.DEFAULT_REPLAYS_TIMEOUT_MS;
+  private String name = AnonymousAuthenticationToken.PRINCIPAL.getUsername();
+
+  public Config() {}
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public long getExpiresAfter() {
+    return expiresAfter;
+  }
+
+  public void setExpiresAfter(long timeout) {
+    this.expiresAfter = timeout;
+  }
+
+  public void setReplayTimeoutSeconds(long seconds) {
+    this.setReplayTimeout(seconds * 1000);
+  }
+
+  public void setExpiresAfterSeconds(long seconds) {
+    this.setExpiresAfter(seconds * 1000);
+  }
+
+  public void setMaxReplays(int maxReplays) {
+    this.maxReplays = maxReplays;
+  }
+
+  public int getMaxReplays() {
+    return maxReplays;
+  }
+
+  public String[] getPermissions() {
+    return permissions;
+  }
+
+  public String getPurpose() {
+    return purpose;
+  }
+
+  public void setPermissions(String[] permissions) {
+    this.permissions = permissions;
+  }
+
+  public String[] getRoles() {
+    return roles;
+  }
+
+  public void setRoles(String[] roles) {
+    this.roles = roles;
+  }
+
+  public void setPurpose(String purpose) {
+    this.purpose = purpose;
+  }
+
+  public OneTimeTokenToFile getOutput() {
+    return output;
+  }
+
+  public void setOutput(OneTimeTokenToFile output) {
+    this.output = output;
+  }
+
+  public long getReplayTimeout() {
+    return replayTimeout;
+  }
+
+  public void setReplayTimeout(long replaysTimeout) {
+    this.replayTimeout = replaysTimeout;
+  }
+}
diff --git a/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java b/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f62fa0a8fc72604d885c24123c3d0c17cb49a0a
--- /dev/null
+++ b/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java
@@ -0,0 +1,29 @@
+package caosdb.server.accessControl;
+
+import caosdb.server.CaosDBServer;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
+
+public class ConsumedInfoCleanupJob implements Job {
+
+  public static void scheduleDaily() throws SchedulerException {
+    JobDetail job = JobBuilder.newJob(ConsumedInfoCleanupJob.class).build();
+    Trigger trigger =
+        TriggerBuilder.newTrigger()
+            .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(0, 0))
+            .build();
+    CaosDBServer.scheduleJob(job, trigger);
+  }
+
+  @Override
+  public void execute(JobExecutionContext context) throws JobExecutionException {
+    OneTimeTokenConsumedInfo.cleanupConsumedInfo();
+  }
+}
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
index ba7dff8d50bd5f77ae4dc035e07caef48d6f3426..8dddf77b092cb951695ecaa15191856a3909793f 100644
--- a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -24,112 +26,240 @@ package caosdb.server.accessControl;
 
 import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
-import java.util.UUID;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import org.apache.shiro.subject.Subject;
 import org.eclipse.jetty.util.ajax.JSON;
+import org.quartz.SchedulerException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToken {
 
-  private static final transient String PEPPER = java.util.UUID.randomUUID().toString();
-  private final String[] permissions;
+  public static final long DEFAULT_MAX_REPLAYS = 1L;
+  public static final int DEFAULT_REPLAYS_TIMEOUT_MS =
+      Integer.parseInt(
+          CaosDBServer.getServerProperty(ServerProperties.KEY_ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS));
+  public static final int DEFAULT_TIMEOUT_MS =
+      Integer.parseInt(
+          CaosDBServer.getServerProperty(ServerProperties.KEY_ONE_TIME_TOKEN_EXPIRES_MS));
+  public static final String REALM_NAME = "OneTimeAuthenticationToken"; // TODO move to UserSources
+  public static final Logger LOGGER = LoggerFactory.getLogger(OneTimeAuthenticationToken.class);
+
+  private long maxReplays;
+  private long replaysTimeout;
 
   public OneTimeAuthenticationToken(
       final Principal principal,
       final long date,
       final long timeout,
       final String salt,
-      final String curry,
       final String checksum,
-      final String... permissions) {
-    super(principal, date, timeout, salt, curry, checksum);
-    this.permissions = permissions;
+      final String[] permissions,
+      final String[] roles,
+      final long maxReplays,
+      final long replaysTimeout) {
+    super(principal, date, timeout, salt, checksum, permissions, roles);
+    this.replaysTimeout = replaysTimeout;
+    this.maxReplays = maxReplays;
+    consume();
+  }
+
+  public long getReplaysTimeout() {
+    return replaysTimeout;
+  }
+
+  public void consume() {
+    OneTimeTokenConsumedInfo.consume(this);
   }
 
   public OneTimeAuthenticationToken(
       final Principal principal,
-      final long date,
       final long timeout,
-      final String salt,
-      final String curry,
-      final String... permissions) {
+      final String[] permissions,
+      final String[] roles,
+      final Long maxReplays,
+      final Long replaysTimeout) {
     super(
         principal,
-        date,
         timeout,
-        salt,
-        curry,
-        calcChecksum(
-            principal.getRealm(),
-            principal.getUsername(),
-            date,
-            timeout,
-            salt,
-            curry,
-            calcChecksum((Object[]) permissions),
-            PEPPER));
-    this.permissions = permissions;
+        permissions,
+        roles,
+        defaultIfNull(maxReplays, DEFAULT_MAX_REPLAYS),
+        defaultIfNull(replaysTimeout, DEFAULT_REPLAYS_TIMEOUT_MS));
   }
 
   private static final long serialVersionUID = -1072740888045267613L;
 
-  public String[] getPermissions() {
-    return this.permissions;
+  /**
+   * Return consumed.
+   *
+   * @param array
+   * @param curry
+   * @return
+   */
+  public static OneTimeAuthenticationToken parse(final Object[] array) {
+    final Principal principal = new Principal((String) array[1], (String) array[2]);
+    final String[] roles = toStringArray((Object[]) array[3]);
+    final String[] permissions = toStringArray((Object[]) array[4]);
+    final long date = (Long) array[5];
+    final long timeout = (Long) array[6];
+    final String salt = (String) array[7];
+    final String checksum = (String) array[8];
+    final long maxReplays = (Long) array[9];
+    final long replaysTimeout = (Long) array[10];
+    return new OneTimeAuthenticationToken(
+        principal, date, timeout, salt, checksum, permissions, roles, maxReplays, replaysTimeout);
+  }
+
+  private static OneTimeAuthenticationToken generate(
+      final Principal principal,
+      final String[] permissions,
+      final String[] roles,
+      final long timeout,
+      final long maxReplays,
+      final long replaysTimeout) {
+
+    return new OneTimeAuthenticationToken(
+        principal, timeout, permissions, roles, maxReplays, replaysTimeout);
+  }
+
+  public static List<Config> loadConfig(InputStream input) throws Exception {
+    List<Config> results = new LinkedList<>();
+    ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+    ObjectReader reader = mapper.readerFor(Config.class);
+    Iterator<Config> configs = reader.readValues(input);
+    configs.forEachRemaining(results::add);
+    return results;
+  }
+
+  public static Map<String, Config> getPurposeMap(List<Config> configs) throws Exception {
+    Map<String, Config> result = new HashMap<>();
+    for (Config config : configs) {
+      if (config.getPurpose() != null && !config.getPurpose().isEmpty()) {
+        if (result.containsKey(config.getPurpose())) {
+          throw new Exception(
+              "OneTimeAuthToken configuration contains duplicate values for the 'purpose' property.");
+        }
+        result.put(config.getPurpose(), config);
+      }
+    }
+    return result;
+  }
+
+  public static OneTimeAuthenticationToken generate(Config c) {
+    return generate(c, new Principal(REALM_NAME, c.getName()));
+  }
+
+  public static OneTimeAuthenticationToken generateForPurpose(String purpose, Subject user) {
+    Config c = purposes.get(purpose);
+    if (c != null) {
+      Principal principal = (Principal) user.getPrincipal();
+
+      return generate(c, principal);
+    }
+    return null;
+  }
+
+  public static OneTimeAuthenticationToken generate(Config c, Principal principal) {
+    return generate(
+        principal,
+        c.getPermissions(),
+        c.getRoles(),
+        c.getExpiresAfter(),
+        c.getMaxReplays(),
+        c.getReplayTimeout());
+  }
+
+  static Map<String, Config> purposes = new HashMap<>();
+
+  public static Map<String, Config> getPurposeMap() {
+    return purposes;
+  }
+
+  public static void initConfig(InputStream yamlConfig) throws Exception {
+    List<Config> configs = loadConfig(yamlConfig);
+    initOutput(configs);
+    purposes.putAll(getPurposeMap(configs));
+  }
+
+  private static void initOutput(List<Config> configs) throws IOException, SchedulerException {
+    for (Config config : configs) {
+      if (config.getOutput() != null) {
+        config.getOutput().init(config);
+      }
+    }
+  }
+
+  public static void initConfig() throws Exception {
+    resetConfig();
+    try (FileInputStream f =
+        new FileInputStream(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_AUTHTOKEN_CONFIG))) {
+      initConfig(f);
+    } catch (IOException e) {
+      LOGGER.error("Could not load the auth token configuration", e);
+    }
   }
 
   @Override
-  public String calcChecksum() {
+  protected void setFields(Object[] fields) {
+    if (fields.length == 2) {
+      this.maxReplays = (long) fields[0];
+      this.replaysTimeout = (long) fields[1];
+    } else {
+      throw new InstantiationError("Too few fields.");
+    }
+  }
+
+  @Override
+  public String calcChecksum(String pepper) {
     return calcChecksum(
-        this.principal.getRealm(),
-        this.principal.getUsername(),
+        "O",
+        this.getRealm(),
+        this.getUsername(),
         this.date,
         this.timeout,
         this.salt,
-        this.curry,
         calcChecksum((Object[]) this.permissions),
-        PEPPER);
+        calcChecksum((Object[]) this.roles),
+        this.maxReplays,
+        this.replaysTimeout,
+        pepper);
   }
 
   @Override
   public String toString() {
     return JSON.toString(
         new Object[] {
-          this.principal.getRealm(),
-          this.principal.getUsername(),
+          "O",
+          this.getRealm(),
+          this.getUsername(),
+          this.roles,
+          this.permissions,
           this.date,
           this.timeout,
           this.salt,
-          this.permissions,
-          this.checksum
+          this.checksum,
+          this.maxReplays,
+          this.replaysTimeout
         });
   }
 
-  private static String[] toStringArray(final Object[] array) {
-    final String[] ret = new String[array.length];
-    for (int i = 0; i < ret.length; i++) {
-      ret[i] = (String) array[i];
-    }
-    return ret;
-  }
-
-  public static OneTimeAuthenticationToken parse(final String token, final String curry) {
-    final Object[] array = (Object[]) JSON.parse(token);
-    final Principal principal = new Principal((String) array[0], (String) array[1]);
-    final long date = (Long) array[2];
-    final long timeout = (Long) array[3];
-    final String salt = (String) array[4];
-    final String[] permissions = toStringArray((Object[]) array[5]);
-    final String checksum = (String) array[6];
-    return new OneTimeAuthenticationToken(
-        principal, date, timeout, salt, curry, checksum, permissions);
+  public long getMaxReplays() {
+    return maxReplays;
   }
 
-  public static OneTimeAuthenticationToken generate(
-      final Principal principal, final String curry, final String... permissions) {
-    return new OneTimeAuthenticationToken(
-        principal,
-        System.currentTimeMillis(),
-        Long.parseLong(CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS)),
-        UUID.randomUUID().toString(),
-        curry,
-        permissions);
+  public static void resetConfig() {
+    purposes.clear();
   }
 }
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..a8ebd2c807712d8b06ec68771e7abb8082ce0696
--- /dev/null
+++ b/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java
@@ -0,0 +1,89 @@
+package caosdb.server.accessControl;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import org.apache.shiro.authc.AuthenticationException;
+
+/**
+ * Utility class to manage OTTs: mark as consumed, removed expired OTTs, manage maximum number of
+ * replays and replay timeout of tokens.
+ */
+class OneTimeTokenConsumedInfo {
+
+  private static Map<String, OneTimeTokenConsumedInfo> consumedOneTimeTokens = new HashMap<>();
+
+  public static void cleanupConsumedInfo() {
+    synchronized (consumedOneTimeTokens) {
+      for (Iterator<Map.Entry<String, OneTimeTokenConsumedInfo>> it =
+              consumedOneTimeTokens.entrySet().iterator();
+          it.hasNext(); ) {
+        Map.Entry<String, OneTimeTokenConsumedInfo> next = it.next();
+        if (next.getValue().isExpired()) {
+          it.remove();
+        }
+      }
+    }
+  }
+
+  /** If the token is valid, consume it once and store this information. */
+  public static void consume(OneTimeAuthenticationToken oneTimeAuthenticationToken) {
+    if (oneTimeAuthenticationToken.isValid()) {
+      String key = OneTimeTokenConsumedInfo.getKey(oneTimeAuthenticationToken);
+      OneTimeTokenConsumedInfo consumedInfo = null;
+      synchronized (consumedOneTimeTokens) {
+        consumedInfo = consumedOneTimeTokens.get(key);
+        if (consumedInfo == null) {
+          consumedInfo = new OneTimeTokenConsumedInfo(oneTimeAuthenticationToken);
+          consumedOneTimeTokens.put(key, consumedInfo);
+        }
+      }
+      consumedInfo.consume();
+    }
+  }
+
+  private OneTimeAuthenticationToken oneTimeAuthenticationToken;
+  private List<Long> replays = new LinkedList<>();
+
+  public OneTimeTokenConsumedInfo(OneTimeAuthenticationToken oneTimeAuthenticationToken) {
+    this.oneTimeAuthenticationToken = oneTimeAuthenticationToken;
+  }
+
+  public static String getKey(OneTimeAuthenticationToken token) {
+    return token.checksum;
+  }
+
+  private int getNoOfReplays() {
+    return replays.size();
+  }
+
+  private long getMaxReplays() {
+    return oneTimeAuthenticationToken.getMaxReplays();
+  }
+
+  private long getReplayTimeout() {
+    if (replays.size() == 0) {
+      return Long.MAX_VALUE;
+    }
+    long firstReplayTime = replays.get(0);
+    return firstReplayTime + oneTimeAuthenticationToken.getReplaysTimeout();
+  }
+
+  /** If there are still replays and time left, increase the replay counter by one. */
+  public void consume() {
+    synchronized (replays) {
+      if (getNoOfReplays() >= getMaxReplays()) {
+        throw new AuthenticationException("One-time token was consumed too often.");
+      } else if (getReplayTimeout() < System.currentTimeMillis()) {
+        throw new AuthenticationException("One-time token replays timeout expired.");
+      }
+      replays.add(System.currentTimeMillis());
+    }
+  }
+
+  public boolean isExpired() {
+    return oneTimeAuthenticationToken.isExpired();
+  }
+}
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java
deleted file mode 100644
index 468a8d8d212ca5546f1940a4b3de38c1da50b3d8..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * ** 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
- *
- * 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
- */
-package caosdb.server.accessControl;
-
-import caosdb.server.accessControl.CaosDBAuthorizingRealm.PermissionAuthenticationInfo;
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.AuthenticationInfo;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
-
-public class OneTimeTokenRealm extends SessionTokenRealm {
-
-  @Override
-  protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
-      throws AuthenticationException {
-
-    final AuthenticationInfo info = super.doGetAuthenticationInfo(token);
-    if (info != null) {
-      return new PermissionAuthenticationInfo(
-          info.getPrincipals(), ((OneTimeAuthenticationToken) token).getPermissions());
-    }
-
-    return null;
-  }
-
-  public OneTimeTokenRealm() {
-    setAuthenticationTokenClass(OneTimeAuthenticationToken.class);
-    setCredentialsMatcher(new AllowAllCredentialsMatcher());
-    setCachingEnabled(false);
-    setAuthenticationCachingEnabled(false);
-    // setAuthorizationCachingEnabled(false);
-  }
-}
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4006783a2105a353749edfaa475bde3dae93808
--- /dev/null
+++ b/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java
@@ -0,0 +1,83 @@
+package caosdb.server.accessControl;
+
+import caosdb.server.CaosDBServer;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+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;
+
+public class OneTimeTokenToFile implements Job {
+  private String file = null;
+  private String schedule = null;
+
+  public OneTimeTokenToFile() {}
+
+  public static void output(OneTimeAuthenticationToken t, String file) throws IOException {
+    output(t, new File(file));
+  }
+
+  public static void output(OneTimeAuthenticationToken t, File file) throws IOException {
+    Files.createParentDirs(file);
+    try (PrintWriter writer = new PrintWriter(file, "utf-8")) {
+      writer.print(t.toString());
+    }
+  }
+
+  public String getFile() {
+    return file;
+  }
+
+  public void setFile(String file) {
+    this.file = file;
+  }
+
+  public String getSchedule() {
+    return schedule;
+  }
+
+  public void setSchedule(String schedule) {
+    this.schedule = schedule;
+  }
+
+  /** If no schedule was set, immediately write the config to file, else schedule the job. */
+  public void init(Config config) throws IOException, SchedulerException {
+
+    if (this.schedule != null) {
+      OneTimeAuthenticationToken.generate(config); // test config, throw away token
+      JobDataMap map = new JobDataMap();
+      map.put("config", config);
+      map.put("file", file);
+      JobDetail outputJob = JobBuilder.newJob(OneTimeTokenToFile.class).setJobData(map).build();
+      Trigger trigger =
+          TriggerBuilder.newTrigger()
+              .withIdentity(config.toString())
+              .withSchedule(CronScheduleBuilder.cronSchedule(this.schedule))
+              .build();
+      CaosDBServer.scheduleJob(outputJob, trigger);
+    } else {
+      output(OneTimeAuthenticationToken.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(OneTimeAuthenticationToken.generate(config), file);
+    } catch (IOException e) {
+      // TODO log
+      e.printStackTrace();
+    }
+  }
+}
diff --git a/src/main/java/caosdb/server/accessControl/Principal.java b/src/main/java/caosdb/server/accessControl/Principal.java
index 3183d864bd5baecc4429cf22fc0ab3ca75d8ca15..6a95dd79eccd50c9358928909822af67056102db 100644
--- a/src/main/java/caosdb/server/accessControl/Principal.java
+++ b/src/main/java/caosdb/server/accessControl/Principal.java
@@ -47,6 +47,11 @@ public class Principal implements ResponsibleAgent {
     this(split[0], split[1]);
   }
 
+  public Principal(Principal principal) {
+    this.username = principal.username;
+    this.realm = principal.realm;
+  }
+
   public String getRealm() {
     return this.realm;
   }
diff --git a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
index 079f592ea7911f974bf4a64c9c5f1674e64a09a5..119a86248b83a472df0d2981db43d9a6cc1962f0 100644
--- a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -23,52 +25,139 @@
 package caosdb.server.accessControl;
 
 import caosdb.server.utils.Utils;
+import java.util.Arrays;
+import java.util.Collection;
+import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.AuthenticationToken;
+import org.eclipse.jetty.util.ajax.JSON;
 
-public abstract class SelfValidatingAuthenticationToken implements AuthenticationToken {
+/**
+ * These AuthenticationTokens are characterized by the following properties:
+ *
+ * <ul>
+ *   <li>date: The creation time.
+ *   <li>timeout: How long this token is valid after creation.
+ *   <li>checksum: The checksum is calculated from all relevant parts of the authentication token
+ *       (including the salt, timeout, permissions, roles, and date) and most importantly, the
+ *       pepper which serves as a randomized password of the server. The salt makes it hard to guess
+ *       the pepper by creating a rainbow table with plausible values for the other properties.
+ *   <li>salt: Salt for the password checksum, may be used by inheriting classes.
+ *   <li>pepper: A static property, generated when class is loaded and used until the server
+ *       reboots. It servers as randomized password of the server. "In cryptography, a pepper is a
+ *       secret added to an input such as a password prior to being hashed with a cryptographic hash
+ *       function." (from: Pepper (cryptography),
+ *       https://en.wikipedia.org/w/index.php?title=Pepper_(cryptography)&oldid=960047694 (last
+ *       visited July 7, 2020)) In our case, the pepper is added to the token before hashing, but
+ *       not exposed to the public, while the salt is. That also means that the resulting hash
+ *       cannot be generated by any client nor be validated by any client, and that all tokens of
+ *       this kind invalidate when the server reboots.
+ */
+public abstract class SelfValidatingAuthenticationToken extends Principal
+    implements AuthenticationToken {
 
+  protected static final transient String PEPPER = Utils.getSecureFilename(32);
   private static final long serialVersionUID = -7212039848895531161L;
+  // date is the token creation time, in ms since 1970
   protected final long date;
+  // token validity duration
   protected final long timeout;
   protected final String checksum;
-  protected final Principal principal;
-  protected final String curry;
   protected final String salt;
+  protected final String[] permissions;
+  protected final String[] roles;
+
+  public Collection<String> getPermissions() {
+    return Arrays.asList(this.permissions);
+  }
+
+  public Collection<String> getRoles() {
+    return Arrays.asList(this.roles);
+  }
+
+  public static final String getFreshSalt() {
+    // salt should be at least 8 octets long. https://www.ietf.org/rfc/rfc2898.txt
+    // let's double that
+    return Utils.getSecureFilename(16);
+  }
+
+  protected static <T> T defaultIfNull(T val, T def) {
+    if (val != null) {
+      return val;
+    }
+    return def;
+  }
 
   public SelfValidatingAuthenticationToken(
       final Principal principal,
       final long date,
       final long timeout,
       final String salt,
-      final String curry,
-      final String checksum) {
-    this.date = date;
-    this.timeout = timeout;
-    this.principal = principal;
-    this.salt = salt;
-    this.curry = curry;
-    this.checksum = checksum;
+      final String checksum,
+      final String[] permissions,
+      final String[] roles) {
+    this(principal, date, timeout, salt, permissions, roles, checksum, false);
   }
 
-  public SelfValidatingAuthenticationToken(
+  private SelfValidatingAuthenticationToken(
       final Principal principal,
       final long date,
       final long timeout,
       final String salt,
-      final String curry) {
+      final String[] permissions,
+      final String[] roles,
+      String checksum,
+      boolean newChecksum,
+      Object... fields) {
+    super(principal);
     this.date = date;
     this.timeout = timeout;
-    this.principal = principal;
     this.salt = salt;
-    this.curry = curry;
-    this.checksum = calcChecksum();
+    this.permissions = defaultIfNull(permissions, new String[] {});
+    this.roles = defaultIfNull(roles, new String[] {});
+    if (fields.length > 0) setFields(fields);
+    // only calculate a checksum iff none is given and newChecksum is explicitly 'true'.
+    this.checksum = checksum == null && newChecksum ? calcChecksum() : checksum;
   }
 
-  @Override
-  public Principal getPrincipal() {
-    return this.principal;
+  /** Customizable customization method, will be called with the remaining constructor arguments. */
+  protected abstract void setFields(Object[] fields);
+
+  public SelfValidatingAuthenticationToken(
+      final Principal principal,
+      final long timeout,
+      final String[] permissions,
+      final String[] roles,
+      Object... fields) {
+    this(
+        principal,
+        System.currentTimeMillis(),
+        timeout,
+        getFreshSalt(),
+        permissions,
+        roles,
+        null,
+        true,
+        fields);
+  }
+
+  public final String calcChecksum() {
+    return calcChecksum(PEPPER);
   }
 
+  @Override
+  public abstract String toString();
+
+  /**
+   * Implementation specific version of a peppered checksum.
+   *
+   * <p>For secure operation, implementing classes must make sure that the pepper is actually used
+   * in calculating the checksum and that the checksum can not be used to infer information about
+   * the pepper. This can be achieved for example by using the {@link calcChecksum(final Object...
+   * fields)} method.
+   */
+  public abstract String calcChecksum(String pepper);
+
+  /** No credentials (returns null), since this token is self-validating. */
   @Override
   public Object getCredentials() {
     return null;
@@ -86,6 +175,9 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio
     return System.currentTimeMillis() >= getExpires();
   }
 
+  /**
+   * Test if the hash stored in `checksum` is equal to the one calculated using the secret pepper.
+   */
   public boolean isHashValid() {
     final String other = calcChecksum();
     return this.checksum != null && this.checksum.equals(other);
@@ -95,8 +187,7 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio
     return !isExpired() && isHashValid();
   }
 
-  public abstract String calcChecksum();
-
+  /** Return the hash (SHA512) of the stringified arguments. */
   protected static String calcChecksum(final Object... fields) {
     final StringBuilder sb = new StringBuilder();
     for (final Object field : fields) {
@@ -107,4 +198,36 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio
     }
     return Utils.sha512(sb.toString(), null, 1);
   }
+
+  protected static String[] toStringArray(final Object[] array) {
+    final String[] ret = new String[array.length];
+    for (int i = 0; i < ret.length; i++) {
+      ret[i] = (String) array[i];
+    }
+    return ret;
+  }
+
+  /**
+   * Parse a JSON string and return the generated token. Depending on the first element of the JSON,
+   * this is either (if it is "O") a OneTimeAuthenticationToken or (if it is "S") a SessionToken.
+   *
+   * @throws AuthenticationToken if the string could not be parsed into a token.
+   */
+  public static SelfValidatingAuthenticationToken parse(String token) {
+    Object[] array = (Object[]) JSON.parse(token);
+    switch (array[0].toString()) {
+      case "O":
+        return OneTimeAuthenticationToken.parse(array);
+      case "S":
+        return SessionToken.parse(array);
+      default:
+        throw new AuthenticationException("Could not parse the authtoken string (unknown type).");
+    }
+  }
+
+  /** No "other" identity, so this returns itself. */
+  @Override
+  public SelfValidatingAuthenticationToken getPrincipal() {
+    return this;
+  }
 }
diff --git a/src/main/java/caosdb/server/accessControl/SessionToken.java b/src/main/java/caosdb/server/accessControl/SessionToken.java
index 273bd50ba7c4ef89fecd0e2b9b8817acbf6d9efa..d24b4afeb739d764947092783c544168a6c0cf3f 100644
--- a/src/main/java/caosdb/server/accessControl/SessionToken.java
+++ b/src/main/java/caosdb/server/accessControl/SessionToken.java
@@ -4,6 +4,9 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2020 Daniel Hornung <d.hornung@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
@@ -24,86 +27,109 @@ package caosdb.server.accessControl;
 
 import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
+import org.apache.shiro.subject.Subject;
 import org.eclipse.jetty.util.ajax.JSON;
 
+/**
+ * Session tokens are formatted as JSON arrays with the following elements:
+ *
+ * <ul>
+ *   <li>Anything but "O" (upper-case "o"), preferred is "S".
+ *   <li>Realm
+ *   <li>name within the Realm
+ *   <li>list of roles
+ *   <li>list of permissions
+ *   <li>time of token generation (long, ms since 1970)
+ *   <li>validity duration (long, ms)
+ *   <li>salt
+ *   <li>checksum
+ */
 public class SessionToken extends SelfValidatingAuthenticationToken {
 
-  /**
-   * Cryptographic pepper. Generated when class is loaded and used until the server reboots. Hence
-   * all SessionTokens invalidate when the server reboots.
-   */
-  private static final transient String PEPPER = java.util.UUID.randomUUID().toString();
-
-  public static final String SEP = ":";
-
   public SessionToken(
       final Principal principal,
       final long date,
       final long timeout,
       final String salt,
-      final String curry,
-      final String checksum) {
-    super(principal, date, timeout, salt, curry, checksum);
+      final String checksum,
+      final String[] permissions,
+      final String[] roles) {
+    super(principal, date, timeout, salt, checksum, permissions, roles);
   }
 
   public SessionToken(
       final Principal principal,
-      final long date,
       final long timeout,
-      final String salt,
-      final String curry) {
-    super(principal, date, timeout, salt, curry);
+      final String[] permissions,
+      final String[] roles) {
+    super(principal, timeout, permissions, roles);
   }
 
   private static final long serialVersionUID = 5887135104218573761L;
 
+  public static SessionToken parse(final Object[] array) {
+    // array[0] is not used here, it was already consumed to determine the type of token.
+    final Principal principal = new Principal((String) array[1], (String) array[2]);
+    final String[] roles = toStringArray((Object[]) array[3]);
+    final String[] permissions = toStringArray((Object[]) array[4]);
+    final long date = (Long) array[5];
+    final long timeout = (Long) array[6];
+    final String salt = (String) array[7];
+    final String checksum = (String) array[8];
+    return new SessionToken(principal, date, timeout, salt, checksum, permissions, roles);
+  }
+
+  private static SessionToken generate(
+      final Principal principal, final String[] permissions, final String[] roles) {
+    int timeout =
+        Integer.parseInt(CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS));
+
+    return new SessionToken(principal, timeout, permissions, roles);
+  }
+
+  public static SessionToken generate(Subject subject) {
+    String[] permissions = new String[] {};
+    String[] roles = new String[] {};
+    if (subject.getPrincipal() instanceof SelfValidatingAuthenticationToken) {
+      SelfValidatingAuthenticationToken p =
+          (SelfValidatingAuthenticationToken) subject.getPrincipal();
+      permissions = p.getPermissions().toArray(permissions);
+      roles = p.getRoles().toArray(roles);
+    }
+    return generate((Principal) subject.getPrincipal(), permissions, roles);
+  }
+
+  /** Nothing to set in this implemention. */
+  @Override
+  protected void setFields(Object[] fields) {}
+
   @Override
-  public String calcChecksum() {
+  public String calcChecksum(String pepper) {
     return calcChecksum(
+        "S",
+        this.getRealm(),
+        this.getUsername(),
         this.date,
         this.timeout,
-        this.principal.getRealm(),
-        this.principal.getUsername(),
-        PEPPER,
         this.salt,
-        this.curry);
+        calcChecksum((Object[]) this.permissions),
+        calcChecksum((Object[]) this.roles),
+        pepper);
   }
 
   @Override
   public String toString() {
     return JSON.toString(
         new Object[] {
-          this.principal.getRealm(),
-          this.principal.getUsername(),
+          "S",
+          this.getRealm(),
+          this.getUsername(),
+          this.roles,
+          this.permissions,
           this.date,
           this.timeout,
           this.salt,
           this.checksum
         });
   }
-
-  public static SessionToken parse(final String token, final String curry) {
-    final Object[] array = (Object[]) JSON.parse(token);
-    final Principal principal = new Principal((String) array[0], (String) array[1]);
-    final long date = (Long) array[2];
-    final long timeout = (Long) array[3];
-    final String salt = (String) array[4];
-    final String checksum = (String) array[5];
-    return new SessionToken(principal, date, timeout, salt, curry, checksum);
-  }
-
-  public static SessionToken generate(final Principal principal, final String curry) {
-    final SessionToken ret =
-        new SessionToken(
-            principal,
-            System.currentTimeMillis(),
-            Long.parseLong(
-                CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS).trim()),
-            java.util.UUID.randomUUID().toString(),
-            curry);
-    if (!ret.isValid()) {
-      throw new RuntimeException("SessionToken not valid!");
-    }
-    return ret;
-  }
 }
diff --git a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
index 6ee72d0295153051e5ad31a6fd0fa092ab53d6e3..cce2850d6d532ae8880d208682b5677dc0369089 100644
--- a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
+++ b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java
@@ -37,13 +37,13 @@ public class SessionTokenRealm extends AuthenticatingRealm {
         (SelfValidatingAuthenticationToken) token;
 
     if (sessionToken.isValid()) {
-      return new SimpleAuthenticationInfo(sessionToken.getPrincipal(), null, getName());
+      return new SimpleAuthenticationInfo(sessionToken, null, getName());
     }
     return null;
   }
 
   public SessionTokenRealm() {
-    setAuthenticationTokenClass(SessionToken.class);
+    setAuthenticationTokenClass(SelfValidatingAuthenticationToken.class);
     setCredentialsMatcher(new AllowAllCredentialsMatcher());
     setCachingEnabled(false);
     setAuthenticationCachingEnabled(false);
diff --git a/src/main/java/caosdb/server/accessControl/UserSources.java b/src/main/java/caosdb/server/accessControl/UserSources.java
index d0f707ebaaec8aa97e9b87d760fc0631cbd2f72a..cdf219c017608c621a59d427b2132630fef0631d 100644
--- a/src/main/java/caosdb/server/accessControl/UserSources.java
+++ b/src/main/java/caosdb/server/accessControl/UserSources.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -25,11 +27,11 @@ package caosdb.server.accessControl;
 import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
 import caosdb.server.entity.Message;
+import caosdb.server.permissions.Role;
 import caosdb.server.transaction.RetrieveRoleTransaction;
 import caosdb.server.transaction.RetrieveUserTransaction;
 import caosdb.server.utils.ServerMessages;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
@@ -37,16 +39,39 @@ import java.util.HashSet;
 import java.util.Set;
 import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.config.Ini;
-import org.apache.shiro.subject.PrincipalCollection;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * This singleton class is the primary resource for authenticating users and resolving principals to
+ * roles.
+ *
+ * <p>Key concepts:
+ *
+ * <ul>
+ *   <li>User name: A string which identifies a user uniquely across one realm. Why is this so?
+ *       Because it is possible, that two different people from collaborating work groups with
+ *       similar names have the same user name in their group e.g. "mueller@uni1.de" and
+ *       "mueller@uni2.de" or two people from different user groups use the name "admin". In the
+ *       "mueller" example the domain name of the email is the realm of authentication.
+ *   <li>Realm: A string which uniquely identifies "where a user comes from". It guarantees the
+ *       authentication of a user with a particular user name. Currently the possible realms are
+ *       quite limited. Only "CaosDB" (which is controlled by the internal user source) and "PAM"
+ *       which delegates authentication to the host system via PAM (Pluggable Authentication Module)
+ *       are known and extension is not too easy.
+ *   <li>User Source: An instance which provides the access to a realm where users can be
+ *       authenticated.
+ *   <li>Principal: The combination of realm and user name - hence a system-wide unique identifier
+ *       for users and the primary key to identifying who did what and who is allowed to to do what.
+ * </ul>
+ *
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
 public class UserSources extends HashMap<String, UserSource> {
 
-  public static final String ANONYMOUS_ROLE = "anonymous";
   private static final Logger logger = LoggerFactory.getLogger(UserSources.class);
   public static final String KEY_DEFAULT_REALM = "defaultRealm";
-  public static final String KEY_REALMS = "defaultRealm";
+  public static final String KEY_REALMS = "realms";
   public static final String KEY_REALM_CLASS = "class";
 
   private static final long serialVersionUID = 6782744064206400521L;
@@ -61,6 +86,11 @@ public class UserSources extends HashMap<String, UserSource> {
   private UserSources() {
     initMap();
     this.put(getInternalRealm());
+    if (this.map.getSection(Ini.DEFAULT_SECTION_NAME) == null
+        || !this.map.getSection(Ini.DEFAULT_SECTION_NAME).containsKey(KEY_REALMS)) {
+      // no realms defined
+      return;
+    }
     final String[] realms =
         this.map
             .getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_REALMS)
@@ -88,6 +118,8 @@ public class UserSources extends HashMap<String, UserSource> {
     }
   }
 
+  private Ini map = null;
+
   public UserSource put(final UserSource src) {
     if (src.getName() == null) {
       throw new IllegalArgumentException("A user source's name must not be null.");
@@ -103,21 +135,14 @@ public class UserSources extends HashMap<String, UserSource> {
     return instance.put(src);
   }
 
-  private Ini map = null;
-
   public void initMap() {
-    this.map = null;
-    try {
-      final FileInputStream f =
-          new FileInputStream(
-              CaosDBServer.getServerProperty(ServerProperties.KEY_USER_SOURCES_INI_FILE));
-      this.map = new Ini();
+    this.map = new Ini();
+    try (final FileInputStream f =
+        new FileInputStream(
+            CaosDBServer.getServerProperty(ServerProperties.KEY_USER_SOURCES_INI_FILE))) {
       this.map.load(f);
-      f.close();
-    } catch (final FileNotFoundException e) {
-      e.printStackTrace();
     } catch (final IOException e) {
-      e.printStackTrace();
+      logger.debug("could not load usersources.ini", e);
     }
   }
 
@@ -126,16 +151,19 @@ public class UserSources extends HashMap<String, UserSource> {
    *
    * @param realm
    * @param username
-   * @return
+   * @return A set of user roles.
    */
-  public static Set<String> resolve(String realm, final String username) {
-
+  public static Set<String> resolveRoles(String realm, final String username) {
     if (realm == null) {
       realm = guessRealm(username);
     }
 
+    UserSource userSource = instance.get(realm);
+    if (userSource == null) {
+      return null;
+    }
     // find all roles that are associated with this principal in this realm
-    final Set<String> ret = instance.get(realm).resolveRolesForUsername(username);
+    final Set<String> ret = userSource.resolveRolesForUsername(username);
 
     return ret;
   }
@@ -165,19 +193,19 @@ public class UserSources extends HashMap<String, UserSource> {
   }
 
   public static String getDefaultRealm() {
-    return instance.map.getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_DEFAULT_REALM);
+    return instance.map.getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_DEFAULT_REALM, "CaosDB");
   }
 
-  public static Set<String> resolve(final PrincipalCollection principals) {
-    if (principals.getPrimaryPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) {
+  // @todo Refactor name: resolveRoles(...)?
+  public static Set<String> resolve(final Principal principal) {
+    if (AnonymousAuthenticationToken.PRINCIPAL == principal) {
       // anymous has one role
       Set<String> roles = new HashSet<>();
-      roles.add(ANONYMOUS_ROLE);
+      roles.add(Role.ANONYMOUS_ROLE.toString());
       return roles;
     }
 
-    Principal primaryPrincipal = (Principal) principals.getPrimaryPrincipal();
-    return resolve(primaryPrincipal.getRealm(), primaryPrincipal.getUsername());
+    return resolveRoles(principal.getRealm(), principal.getUsername());
   }
 
   public static boolean isRoleExisting(final String role) {
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
deleted file mode 100644
index 30dd77e0c0fc7f97f85db7d45c00cf762badc695..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
+++ /dev/null
@@ -1,562 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import static caosdb.server.permissions.Role.ANONYMOUS_ROLE;
-
-import caosdb.server.accessControl.UserSources;
-import caosdb.server.database.exceptions.EntityDoesNotExistException;
-import caosdb.server.datatype.SingleValue;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.Message;
-import caosdb.server.entity.Message.MessageType;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.core.CheckPropValid;
-import caosdb.server.permissions.EntityACL;
-import caosdb.server.permissions.EntityACLFactory;
-import caosdb.server.permissions.EntityPermission;
-import caosdb.server.query.Query;
-import caosdb.server.transaction.Delete;
-import caosdb.server.transaction.Insert;
-import caosdb.server.transaction.Update;
-import caosdb.server.utils.EntityStatus;
-import caosdb.server.utils.ServerMessages;
-import caosdb.server.utils.Utils;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@JobAnnotation(transaction = caosdb.server.transaction.WriteTransaction.class, loadAlways = true)
-public class AWIBoxLoan extends AWIBoxLoanModel {
-
-  public Logger logger = LoggerFactory.getLogger(getClass());
-  private static final Message UNIQUE_USER =
-      new Message("The user must have a unique combination of first name and last name!");
-  private static final Message BOX_HAS_LOAN =
-      new Message(
-          "This box cannot be be requested right now because it appears to have a Loan property attached to it. This usually means, that the box is already requested or borrowed by someone.");
-
-  @Override
-  protected void run() {
-    try {
-      if (isAnonymous()
-          && (getTransaction() instanceof Delete
-              || isAcceptBorrowUpdateLoan()
-              || isConfirmLoanUpdateLoan()
-              || isRejectReturnUpdateLoan()
-              || isAcceptReturnUpdateLoan()
-              || isManualReturnUpdateBox()
-              || isManualReturnUpdateLoan()
-              || !(isRequestLoanSetUser()
-                  || isRequestLoanInsertLoan()
-                  || isRequestLoanUpdateBox()
-                  || isRequestReturnSetUser()
-                  || isRequestReturnUpdateLoan()))) {
-        addError(ServerMessages.AUTHORIZATION_ERROR);
-        getContainer()
-            .addMessage(
-                new Message(MessageType.Info, 0, "Anonymous users have restricted permissions."));
-        return;
-      }
-    } catch (EntityDoesNotExistException exc) {
-      addError(ServerMessages.AUTHORIZATION_ERROR);
-      getContainer()
-          .addMessage(
-              new Message(MessageType.Info, 0, "Anonymous users have restricted permissions."));
-      return;
-    }
-
-    try {
-      if (!(getTransaction() instanceof Delete
-          || isRequestLoanSetUser()
-          || isRequestLoanInsertLoan()
-          || isManualReturnUpdateLoan()
-          || isManualReturnUpdateBox()
-          || isRequestLoanUpdateBox()
-          || isAcceptBorrowUpdateLoan()
-          || isConfirmLoanUpdateLoan()
-          || isRequestReturnSetUser()
-          || isRequestReturnUpdateLoan()
-          || isRejectReturnUpdateLoan())) {
-        isAcceptReturnUpdateLoan();
-      }
-    } catch (EntityDoesNotExistException exc) {
-      // ignore
-    }
-
-    // special ACL for boxes, loans and persons
-    try {
-      if (getTransaction() instanceof Insert) {
-        for (EntityInterface e : getContainer()) {
-          if (isBoxRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getBoxACL()));
-          } else if (isLoanRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getLoanACL()));
-          } else if (isPersonRecord(e)) {
-            e.setEntityACL(EntityACL.combine(e.getEntityACL(), getPersonACL()));
-          }
-        }
-      }
-    } catch (EntityDoesNotExistException e) {
-    }
-  }
-
-  boolean isManualReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasManualReturnLoanProperties(e)) {
-        logger.trace("isManualReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    for (EntityInterface e : getContainer()) {
-      removeAnonymousPermissions(e);
-    }
-    logger.trace("isManualReturnUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasManualReturnLoanProperties(EntityInterface e) {
-    for (Property p : e.getProperties()) {
-      if (isReturnedProperty(p)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  boolean isReturnedProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnedId());
-  }
-
-  void removeAnonymousPermissions(EntityInterface e) {
-    EntityACLFactory f = new EntityACLFactory();
-    f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    e.setEntityACL(EntityACL.combine(e.getEntityACL(), f.create()));
-  }
-
-  boolean isManualReturnUpdateBox() {
-    for (EntityInterface e : getContainer()) {
-      if (!isBoxRecord(e) || !hasManualReturnBoxProperties(e)) {
-        logger.trace("isManualReturnUpdateBox: false");
-        return false;
-      }
-    }
-    logger.trace("isManualReturnUpdateBox: true");
-    return true;
-  }
-
-  boolean hasManualReturnBoxProperties(EntityInterface e) {
-    return validBoxHasLoanProperty(e) && boxLoanHasReturnedProperty(e);
-  }
-
-  boolean boxLoanHasReturnedProperty(EntityInterface e) {
-    try {
-      EntityInterface validBox = retrieveValidEntity(e.getId());
-
-      for (Property p : validBox.getProperties()) {
-        if (isLoanProperty(p)) {
-          return validLoanHasReturnedProperty(p.getId());
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  boolean validLoanHasReturnedProperty(Integer id) {
-    try {
-      EntityInterface validLoan = retrieveValidEntity(id);
-      for (Property p : validLoan.getProperties()) {
-        if (isReturnedProperty(p)) {
-          return true;
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  boolean isLoanProperty(Property p) {
-    return Objects.equals(p.getId(), getLoanId());
-  }
-
-  private boolean isRejectReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasRejectReturnProperties(e)) {
-        logger.trace("isRejectReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    for (EntityInterface e : getContainer()) {
-      removeDestinationProperty(e);
-    }
-    logger.trace("isRejectReturnUpdateLoan: true");
-    return true;
-  }
-
-  void removeDestinationProperty(EntityInterface e) {
-    Iterator<Property> iterator = e.getProperties().iterator();
-    while (iterator.hasNext()) {
-      Property p = iterator.next();
-      if (isDestinationProperty(p)) {
-        iterator.remove();
-      }
-    }
-  }
-
-  boolean hasRejectReturnProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isDestinationProperty(p)) {
-        found = true;
-      } else if (isReturnAcceptedProperty(p) || isReturnRequestProperty(p)) {
-        logger.trace("hasRejectReturnProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasRejectReturnProperties found: {}", found);
-    return found;
-  }
-
-  boolean isDestinationProperty(Property p) {
-    return Objects.equals(p.getId(), getDestinationId());
-  }
-
-  boolean isAcceptReturnUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasAcceptReturnProperties(e)) {
-        logger.trace("isAcceptReturnUpdateLoan: false");
-        return false;
-      }
-    }
-    logger.trace("isAcceptReturnUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasAcceptReturnProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isReturnAcceptedProperty(p)) {
-        found = true;
-      }
-    }
-    return found;
-  }
-
-  boolean isConfirmLoanUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasConfirmLoanProperties(e)) {
-        logger.trace("isConfirmLoanUpdateLoan: false");
-        return false;
-      }
-    }
-    // switch from destination to location
-    for (EntityInterface e : getContainer()) {
-      switchDestinationToLocation(e);
-    }
-    logger.trace("isConfirmLoanUpdateLoan: true");
-    return true;
-  }
-
-  void switchDestinationToLocation(EntityInterface e) {
-    Iterator<Property> iterator = e.getProperties().iterator();
-    EntityInterface p = retrieveValidEntity(getLocationId());
-    while (iterator.hasNext()) {
-      Property next = iterator.next();
-      if (isDestinationProperty(next)) {
-        iterator.remove();
-        p.setValue(next.getValue());
-        e.addProperty(new Property(p));
-        break;
-      }
-    }
-  }
-
-  boolean hasConfirmLoanProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isLentProperty(p)) {
-        found = true;
-      } else if (isReturnAcceptedProperty(p) || isDestinationProperty(p)) {
-        logger.trace("hasConfirmLoanProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasConfirmLoanProperties found: {}", found);
-    return found;
-  }
-
-  boolean isAcceptBorrowUpdateLoan() {
-    for (EntityInterface e : getContainer()) {
-      if (!isLoanRecord(e) || !hasLoanAcceptProperties(e)) {
-        logger.trace("isAcceptBorrowUpdateLoan: false");
-        return false;
-      }
-    }
-    logger.trace("isAcceptBorrowUpdateLoan: true");
-    return true;
-  }
-
-  boolean hasLoanAcceptProperties(EntityInterface e) {
-    boolean found = false;
-    for (Property p : e.getProperties()) {
-      if (isLoanAcceptProperty(p)) {
-        found = true;
-      } else if (isLentProperty(p) || isReturnAcceptedProperty(p)) {
-        logger.trace("hasLoanAcceptProperties: false");
-        return false;
-      }
-    }
-    logger.trace("hasLoanAcceptProperties found: {}", found);
-    return found;
-  }
-
-  boolean isReturnAcceptedProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnAcceptedId());
-  }
-
-  boolean isLentProperty(Property p) {
-    return Objects.equals(p.getId(), getLentId());
-  }
-
-  boolean isLoanAcceptProperty(Property p) {
-    return Objects.equals(p.getId(), getLoanAcceptedId());
-  }
-
-  EntityACL getPersonACL() {
-    // same as loan acl - property updates are allowed for anonymous.
-    return getLoanACL();
-  }
-
-  EntityACL getLoanACL() {
-    EntityACLFactory f = new EntityACLFactory();
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    return f.create();
-  }
-
-  EntityACL getBoxACL() {
-    EntityACLFactory f = new EntityACLFactory();
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY);
-    f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY);
-    return f.create();
-  }
-
-  boolean isAnonymous() {
-    boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE);
-    logger.trace("is Anonymous: {}", ret);
-    return ret;
-  }
-
-  boolean isRequestReturnUpdateLoan() {
-    // is UPDATE transaction
-    if (getTransaction() instanceof Update) {
-      // Container has only loan elements with special properties
-      for (EntityInterface e : getContainer()) {
-        if (!isLoanRecord(e) || !hasOnlyAllowedLoanProperties4RequestReturn(e)) {
-          logger.trace("isRequestReturnUpdateLoan: false");
-          return false;
-        }
-        setReturnRequestedDate(e);
-      }
-      appendJob(AWIBoxLoanRequestReturnCuratorEmail.class);
-      logger.trace("isRequestReturnUpdateLoan: true");
-      return true;
-    }
-    logger.trace("isRequestReturnUpdateLoan: false");
-    return false;
-  }
-
-  boolean isRequestReturnSetUser() {
-    // same as request_loan.set_user
-    logger.trace("isRequestReturnSetUser: ->");
-    return isRequestLoanSetUser();
-  }
-
-  boolean isRequestLoanUpdateBox() {
-    // is UPDATE transaction
-    if (getTransaction() instanceof Update) {
-      // Container has only box elements
-      for (EntityInterface e : getContainer()) {
-        if (validBoxHasLoanProperty(e)) {
-          e.addError(BOX_HAS_LOAN);
-          return false;
-        }
-        if (!isBoxRecord(e) || !hasOnlyAllowedBoxProperties4RequestLoan(e)) {
-          return false;
-        }
-        // TODO this breaks the box loan functionality if any other prior changes have been made to
-        // the box
-        //        appendJob(e, CheckNoAdditionalPropertiesPresent.class);
-      }
-      return true;
-    }
-    return false;
-  }
-
-  boolean validBoxHasLoanProperty(EntityInterface e) {
-    try {
-      EntityInterface validBox = retrieveValidEntity(e.getId());
-      for (Property p : validBox.getProperties()) {
-        if (isLoanProperty(p)) {
-          return true;
-        }
-      }
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-    return false;
-  }
-
-  /** Has only one new property -> Loan. */
-  boolean hasOnlyAllowedBoxProperties4RequestLoan(EntityInterface e) {
-    int count = 0;
-    for (Property p : e.getProperties()) {
-      if (p.getEntityStatus() == EntityStatus.QUALIFIED && Objects.equals(p.getId(), getLoanId())) {
-        count++;
-      }
-    }
-
-    // Box has only one update, a loan property
-    return count == 1;
-  }
-
-  boolean isRequestLoanInsertLoan() {
-    // is INSERT transaction
-    if (getTransaction() instanceof Insert) {
-      // Container has only loan elements
-      for (EntityInterface e : getContainer()) {
-        if (!isLoanRecord(e)) {
-          return false;
-        }
-      }
-      for (EntityInterface e : getContainer()) {
-        if (isAnonymous()) {
-          setCuratorAsOwner(e);
-        }
-        setLoanRequestDate(e);
-        // TODO this check breaks the box loan functionality if any other changes have been made to
-        // the box entity
-        //        appendJob(e, CheckNoAdditionalPropertiesPresent.class);
-        //        appendJob(e, CheckNoOverridesPresent.class);
-      }
-      appendJob(AWIBoxLoanRequestLoanCuratorEmail.class);
-      return true;
-    }
-    return false;
-  }
-
-  void setCuratorAsOwner(EntityInterface e) {
-    e.setEntityACL(EntityACL.getOwnerACLFor(caosdb.server.permissions.Role.create("curator")));
-  }
-
-  void setReturnRequestedDate(EntityInterface e) {
-    // TODO setDateProperty(e, getReturnRequestedId());
-  }
-
-  private void setDateProperty(EntityInterface e, Integer propertyId) {
-    // TODO
-    EntityInterface p = retrieveValidEntity(propertyId);
-    p.setValue(getTransaction().getTimestamp());
-    e.addProperty(new Property(p));
-  }
-
-  void setLoanRequestDate(EntityInterface e) {
-    // TODO setDateProperty(e, getLoanRequestedId());
-  }
-
-  boolean isRequestLoanSetUser() {
-    // is INSERT/UPDATE transaction
-    // Container has only one element, user
-    if ((getTransaction() instanceof Update || getTransaction() instanceof Insert)
-        && getContainer().size() == 1
-        && isPersonRecord(getContainer().get(0))
-        && checkUniqueName(getContainer().get(0))
-        && checkEmail(getContainer().get(0))) {
-      // TODO this check breaks the box loan functionality if any other changes have been made to
-      // the box entity
-      //      appendJob(getContainer().get(0), CheckNoAdditionalPropertiesPresent.class);
-      //      appendJob(getContainer().get(0), CheckNoOverridesPresent.class);
-      logger.trace("isRequestReturnSetUser: true");
-      return true;
-    }
-    logger.trace("isRequestReturnSetUser: false");
-    return false;
-  }
-
-  boolean checkEmail(EntityInterface entity) {
-    runJobFromSchedule(entity, CheckPropValid.class);
-    for (Property p : entity.getProperties()) {
-      if (Objects.equals(p.getId(), getEmailID()) && p.getValue() instanceof SingleValue) {
-        if (!Utils.isRFC822Compliant(((SingleValue) p.getValue()).toDatabaseString())) {
-          p.addError(ServerMessages.EMAIL_NOT_WELL_FORMED);
-        } else {
-          p.setName("email"); // TODO fix webinterface to use lower-case email
-          p.setNameOverride(false);
-        }
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private boolean checkUniqueName(EntityInterface entity) {
-    String firstName = null;
-    String lastName = null;
-    Query q =
-        new Query(
-                "FIND "
-                    + getPersonID().toString()
-                    + " WITH "
-                    + getFirstNameId().toString()
-                    + "='"
-                    + firstName
-                    + "' AND "
-                    + getLastNameId().toString()
-                    + "='"
-                    + lastName
-                    + "'",
-                getUser())
-            .execute(getTransaction().getAccess());
-    List<Integer> resultSet = q.getResultSet();
-    if (resultSet.isEmpty()
-        || (resultSet.size() == 1 && Objects.equals(resultSet.get(0), entity.getId()))) {
-      return true;
-    }
-    entity.addError(UNIQUE_USER);
-    return false;
-  }
-
-  /**
-   * Has only 5/6 new/updated properties: content, returnRequested, destination, Borrower, comment
-   * (optional), location
-   *
-   * @throws Message
-   */
-  boolean hasOnlyAllowedLoanProperties4RequestReturn(EntityInterface e) {
-    runJobFromSchedule(e, CheckPropValid.class);
-    //    appendJob(e, CheckNoOverridesPresent.class);
-
-    boolean foundReturnRequested = false;
-    for (Property p : e.getProperties()) {
-      if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update
-        if (isReturnRequestProperty(p)) {
-          foundReturnRequested = true;
-        } else if (isReturnAcceptedProperty(p) || isReturnedProperty(p)) {
-          logger.trace("hasOnlyAllowedLoanProperties4RequestReturn: false");
-          return false; // this is not a returnRequest, return has already been accepted
-        }
-      }
-    }
-    logger.trace("hasOnlyAllowedLoanProperties4RequestReturn found: {}", foundReturnRequested);
-    return foundReturnRequested;
-  }
-
-  boolean isReturnRequestProperty(Property p) {
-    return Objects.equals(p.getId(), getReturnRequestedId());
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java
deleted file mode 100644
index 2bf51e76e7357839ee0c52d28caa18017691849f..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.datetime.DateTimeInterface;
-import caosdb.server.CaosDBServer;
-import caosdb.server.ServerProperties;
-import caosdb.server.datatype.ReferenceValue;
-import caosdb.server.datatype.SingleValue;
-import caosdb.server.datatype.Value;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.utils.mail.Mail;
-import java.util.TimeZone;
-
-public abstract class AWIBoxLoanCuratorEmail extends AWIBoxLoanModel {
-
-  public static final String KEY_EXT_AWI_CURATOR_EMAIL = "EXT_AWI_BOX_CURATOR_EMAIL";
-  protected static final String FROM_EMAIL =
-      CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_EMAIL);
-  protected static final String FROM_NAME =
-      CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_NAME);
-  protected static final String CURATOR_EMAIL =
-      CaosDBServer.getServerProperty(KEY_EXT_AWI_CURATOR_EMAIL);
-
-  protected String loanToString(EntityInterface e) {
-    StringBuilder s = new StringBuilder();
-    for (Property p : e.getProperties()) {
-      s.append("\n");
-      s.append(p.getName());
-      s.append(": ");
-      if (p.getValue() instanceof ReferenceValue) {
-        Integer id = ((ReferenceValue) p.getValue()).getId();
-        if (isBorrowerProperty(p)) {
-          s.append(borrowerToString(id));
-        } else if (isBoxProperty(p)) {
-          s.append(boxToString(id));
-        } else {
-          s.append(valueToString(p.getValue()));
-        }
-      } else {
-        s.append(valueToString(p.getValue()));
-      }
-    }
-    return s.toString();
-  }
-
-  private String valueToString(Value value) {
-    if (value == null) {
-      return "";
-    }
-    if (value instanceof DateTimeInterface) {
-      return ((DateTimeInterface) value).toDateTimeString(TimeZone.getDefault());
-    }
-    if (value instanceof SingleValue) {
-      return ((SingleValue) value).toDatabaseString();
-    } else {
-      return value.toString();
-    }
-  }
-
-  private String boxToString(Integer id) {
-    StringBuilder s = new StringBuilder();
-    EntityInterface box = retrieveValidEntity(id);
-    for (Property sp : box.getProperties()) {
-      if (isNumberProperty(sp) || isContentProperty(sp) || isLocationProperty(sp)) {
-        s.append("\n  ");
-        s.append(sp.getName());
-        s.append(": ");
-        s.append(valueToString(sp.getValue()));
-      }
-    }
-    return s.toString();
-  }
-
-  private String borrowerToString(Integer id) {
-    StringBuilder s = new StringBuilder();
-    EntityInterface borrower = retrieveValidEntity(id);
-    for (Property sp : borrower.getProperties()) {
-      s.append("\n  ");
-      s.append(sp.getName());
-      s.append(": ");
-      s.append(valueToString(sp.getValue()));
-    }
-    return s.toString();
-  }
-
-  protected void sendCuratorEmail(String body, String subject) {
-    for (String addr : CURATOR_EMAIL.split(" ")) {
-      Mail m = new Mail(FROM_NAME, FROM_EMAIL, null, addr, subject, body);
-      m.send();
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java
deleted file mode 100644
index 92a8a240363cf8659e0d3526dec1c5fcac8ddd11..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.CaosDBServer;
-import caosdb.server.database.exceptions.EntityDoesNotExistException;
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.entity.Role;
-import caosdb.server.entity.wrapper.Property;
-import caosdb.server.jobs.ContainerJob;
-import caosdb.server.utils.Utils;
-import java.util.Objects;
-
-public abstract class AWIBoxLoanModel extends ContainerJob {
-
-  /** Is Record and has single user parent. */
-  boolean isPersonRecord(EntityInterface entity) {
-    try {
-      return entity.getParents().size() == 1
-          && Objects.equals(
-              retrieveValidIDByName(entity.getParents().get(0).getName()), getPersonID());
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-  }
-
-  /** Is Record an has single box parent. */
-  boolean isBoxRecord(EntityInterface e) {
-    return e.getRole() == Role.Record
-        && e.getParents().size() == 1
-        && Objects.equals(e.getParents().get(0).getId(), getBoxId());
-  }
-
-  /** Is Record and has single loan parent */
-  boolean isLoanRecord(EntityInterface e) {
-    try {
-      return e.getRole() == Role.Record
-          && e.getParents().size() == 1
-          && Objects.equals(retrieveValidIDByName(e.getParents().get(0).getName()), getLoanId());
-    } catch (EntityDoesNotExistException exc) {
-      return false;
-    }
-  }
-
-  boolean isBoxProperty(Property p) {
-    return Objects.equals(p.getId(), getBoxId());
-  }
-
-  boolean isBorrowerProperty(Property p) {
-    return Objects.equals(p.getId(), getBorrowerId());
-  }
-
-  boolean isLocationProperty(Property p) {
-    return Objects.equals(p.getId(), getLocationId());
-  }
-
-  boolean isContentProperty(Property p) {
-    return Objects.equals(p.getId(), getContentId());
-  }
-
-  boolean isNumberProperty(Property p) {
-    return Objects.equals(p.getId(), getNumberId());
-  }
-
-  Integer getIdOf(String string) {
-    String id = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_ID");
-    if (id != null && Utils.isNonNullInteger(id)) {
-      return Integer.parseInt(id);
-    }
-    String name = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_NAME");
-    if (name == null || name.isEmpty()) {
-      name = string;
-    }
-    return retrieveValidIDByName(name);
-  }
-
-  Integer getBorrowerId() {
-    return getIdOf("Borrower");
-  }
-
-  Integer getCommentId() {
-    return getIdOf("comment");
-  }
-
-  Integer getLocationId() {
-    return getIdOf("Location");
-  }
-
-  Integer getDestinationId() {
-    return getIdOf("destination");
-  }
-
-  Integer getContentId() {
-    return getIdOf("Content");
-  }
-
-  Integer getBoxId() {
-    return getIdOf("Box");
-  }
-
-  Integer getLoanId() {
-    return getIdOf("Loan");
-  }
-
-  Integer getPersonID() {
-    return getIdOf("Person");
-  }
-
-  Integer getEmailID() {
-    return getIdOf("email");
-  }
-
-  Integer getLoanAcceptedId() {
-    return getIdOf("loanAccepted");
-  }
-
-  Integer getReturnAcceptedId() {
-    return getIdOf("returnAccepted");
-  }
-
-  Integer getLentId() {
-    return getIdOf("lent");
-  }
-
-  Integer getLoanRequestedId() {
-    return getIdOf("loanRequested");
-  }
-
-  Integer getExhaustContentsId() {
-    return getIdOf("exhaustContents");
-  }
-
-  Integer getExpectedReturnId() {
-    return getIdOf("expectedReturn");
-  }
-
-  Integer getReturnRequestedId() {
-    return getIdOf("returnRequested");
-  }
-
-  Integer getLastNameId() {
-    return getIdOf("lastName");
-  }
-
-  Integer getFirstNameId() {
-    return getIdOf("firstName");
-  }
-
-  Integer getNumberId() {
-    return getIdOf("Number");
-  }
-
-  Integer getReturnedId() {
-    return getIdOf("returned");
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java
deleted file mode 100644
index ff490e99a72e93b03b04a0134c81790b1a2a1bec..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.JobExecutionTime;
-
-@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION)
-public class AWIBoxLoanRequestLoanCuratorEmail extends AWIBoxLoanCuratorEmail {
-
-  private static final String SUBJECT = "Box Loan Requests";
-
-  @Override
-  protected void run() {
-    StringBuilder body = new StringBuilder("LOAN REQUEST(S):");
-
-    if (!getContainer().isEmpty()) {
-      for (EntityInterface e : getContainer()) {
-        body.append("\n");
-        body.append(loanToString(e));
-      }
-      this.sendCuratorEmail(body.toString(), SUBJECT);
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java
deleted file mode 100644
index 373ce0b8d10ef6db560decb6afcc3e7ab5aa71b3..0000000000000000000000000000000000000000
--- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import caosdb.server.entity.EntityInterface;
-import caosdb.server.jobs.JobAnnotation;
-import caosdb.server.jobs.JobExecutionTime;
-
-@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION)
-public class AWIBoxLoanRequestReturnCuratorEmail extends AWIBoxLoanCuratorEmail {
-  private static final String SUBJECT = "Box Return Requests";
-
-  @Override
-  protected void run() {
-    if (!getContainer().isEmpty()) {
-      StringBuilder body = new StringBuilder("RETURN REQUEST(S):");
-      for (EntityInterface e : getContainer()) {
-        body.append("\n");
-        body.append(loanToString(e));
-      }
-      this.sendCuratorEmail(body.toString(), SUBJECT);
-    }
-  }
-}
diff --git a/src/main/java/caosdb/server/permissions/EntityACL.java b/src/main/java/caosdb/server/permissions/EntityACL.java
index 34ef34351826facfa7d342d6c8b06e8d8c27cf74..e0bfcf9243eb436f9af0d6a3bacfabb79984538c 100644
--- a/src/main/java/caosdb/server/permissions/EntityACL.java
+++ b/src/main/java/caosdb/server/permissions/EntityACL.java
@@ -92,7 +92,11 @@ public class EntityACL {
   }
 
   public static final EntityACL getOwnerACLFor(final Subject subject) {
-    if (subject.getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) {
+    // TODO handle the case where a valid non-guest user (e.g. me@PAM,
+    // you@CaosDB) is (just temporarily) authenticated via a
+    // OneTimeAuthenticationToken
+    if (AuthenticationUtils.isAnonymous(subject)
+        || AuthenticationUtils.isFromOneTimeTokenRealm(subject)) {
       return new EntityACLFactory().create();
     }
     return getOwnerACLFor((Principal) subject.getPrincipal());
diff --git a/src/main/java/caosdb/server/permissions/Role.java b/src/main/java/caosdb/server/permissions/Role.java
index 70e1a61f754b4beeffe8f8fe203b42842d49cb6d..87b5d2474c4d2d3f13fd2787aaf5b11a1245a39c 100644
--- a/src/main/java/caosdb/server/permissions/Role.java
+++ b/src/main/java/caosdb/server/permissions/Role.java
@@ -22,7 +22,6 @@
  */
 package caosdb.server.permissions;
 
-import caosdb.server.accessControl.UserSources;
 import java.util.HashMap;
 import org.jdom2.Attribute;
 import org.jdom2.Element;
@@ -31,7 +30,7 @@ public class Role implements ResponsibleAgent {
 
   public static final Role OWNER_ROLE = new Role("?OWNER?");
   public static final Role OTHER_ROLE = new Role("?OTHER?");
-  public static final Role ANONYMOUS_ROLE = new Role(UserSources.ANONYMOUS_ROLE);
+  public static final Role ANONYMOUS_ROLE = new Role("anonymous");
 
   private final String role;
 
diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
index ea4e65f0a60d72cb5da6cb03b2cec44848dbc3c6..d7897450b2caaa7a70f20ead02ed1bb0c67e192d 100644
--- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
+++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java
@@ -29,7 +29,6 @@ import static java.net.URLDecoder.decode;
 import caosdb.server.CaosDBException;
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.Principal;
-import caosdb.server.accessControl.UserSources;
 import caosdb.server.database.backend.implementation.MySQL.ConnectionException;
 import caosdb.server.entity.Message;
 import caosdb.server.utils.ServerMessages;
@@ -210,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);
@@ -231,7 +228,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
    * @param user
    */
   private void addRoles(Element userInfo, Subject user) {
-    Collection<String> roles = UserSources.resolve(user.getPrincipals());
+    Collection<String> roles = AuthenticationUtils.getRoles(user);
     if (roles == null) return;
     Element rs = new Element("Roles");
     for (String role : roles) {
@@ -248,8 +245,10 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
    * @param userInfo
    */
   private void addNameAndRealm(Element userInfo, Subject user) {
-    userInfo.setAttribute("username", ((Principal) user.getPrincipal()).getUsername());
-    userInfo.setAttribute("realm", ((Principal) user.getPrincipal()).getRealm());
+    if (!AuthenticationUtils.isAnonymous((Principal) user.getPrincipal())) {
+      userInfo.setAttribute("username", ((Principal) user.getPrincipal()).getUsername());
+      userInfo.setAttribute("realm", ((Principal) user.getPrincipal()).getRealm());
+    }
   }
 
   @Get
@@ -409,11 +408,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/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java
index b23a9593918ead75a6b38216838995e09ac67bc7..cb652b862282e2cf804ae4cbffd3b6f9de9d109f 100644
--- a/src/main/java/caosdb/server/resource/ScriptingResource.java
+++ b/src/main/java/caosdb/server/resource/ScriptingResource.java
@@ -25,12 +25,13 @@
 package caosdb.server.resource;
 
 import caosdb.server.FileSystem;
-import caosdb.server.accessControl.Principal;
+import caosdb.server.accessControl.AuthenticationUtils;
+import caosdb.server.accessControl.OneTimeAuthenticationToken;
 import caosdb.server.accessControl.SessionToken;
-import caosdb.server.accessControl.UserSources;
 import caosdb.server.entity.FileProperties;
 import caosdb.server.entity.Message;
 import caosdb.server.scripting.CallerSerializer;
+import caosdb.server.scripting.ScriptingPermissions;
 import caosdb.server.scripting.ServerSideScriptingCaller;
 import caosdb.server.utils.Serializer;
 import caosdb.server.utils.ServerMessages;
@@ -48,6 +49,7 @@ import org.apache.commons.fileupload.FileItemIterator;
 import org.apache.commons.fileupload.FileItemStream;
 import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.shiro.subject.Subject;
 import org.jdom2.Element;
 import org.restlet.data.CharacterSet;
 import org.restlet.data.Form;
@@ -83,9 +85,6 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
   @Override
   protected Representation httpPostInChildClass(Representation entity) throws Exception {
 
-    if (isAnonymous()) {
-      return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN);
-    }
     MediaType mediaType = entity.getMediaType();
     try {
       if (mediaType.equals(MediaType.MULTIPART_FORM_DATA, true)) {
@@ -193,28 +192,42 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
 
   public int callScript(Form form, List<FileProperties> files) throws Message {
     List<String> commandLine = form2CommandLine(form);
+    String call = commandLine.get(0);
+
+    checkExecutionPermission(getUser(), call);
     Integer timeoutMs = Integer.valueOf(form.getFirstValue("timeout", "-1"));
     return callScript(commandLine, timeoutMs, files);
   }
 
   public int callScript(List<String> commandLine, Integer timeoutMs, List<FileProperties> files)
       throws Message {
-    return callScript(commandLine, timeoutMs, files, generateAuthToken());
+    return callScript(commandLine, timeoutMs, files, generateAuthToken(commandLine.get(0)));
+  }
+
+  public boolean isAnonymous() {
+    return AuthenticationUtils.isAnonymous(getUser());
   }
 
-  public Object generateAuthToken() {
-    return SessionToken.generate((Principal) getUser().getPrincipal(), null);
+  /**
+   * Generate and return a token for the purpose of the given call. If the user is not anonymous and
+   * the call is not configured to be called by everyone, a SessionToken is returned instead.
+   */
+  public Object generateAuthToken(String call) {
+    String purpose = "SCRIPTING:EXECUTE:" + call;
+    Object authtoken = OneTimeAuthenticationToken.generateForPurpose(purpose, getUser());
+    if (authtoken != null || isAnonymous()) {
+      return authtoken;
+    }
+    return SessionToken.generate(getUser());
   }
 
-  boolean isAnonymous() {
-    boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE);
-    return ret;
+  public void checkExecutionPermission(Subject user, String call) {
+    user.checkPermission(ScriptingPermissions.PERMISSION_EXECUTION(call));
   }
 
   public int callScript(
       List<String> commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken)
       throws Message {
-    // TODO getUser().checkPermission("SCRIPTING:EXECUTE:" + commandLine.get(0));
     caller =
         new ServerSideScriptingCaller(
             commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, authToken);
diff --git a/src/main/java/caosdb/server/scripting/ScriptingPermissions.java b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java
new file mode 100644
index 0000000000000000000000000000000000000000..9165f133c070c351e7a4ecbb2c510c06b71734a2
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java
@@ -0,0 +1,11 @@
+package caosdb.server.scripting;
+
+public class ScriptingPermissions {
+
+  public static final String PERMISSION_EXECUTION(final String call) {
+    StringBuilder ret = new StringBuilder(18 + call.length());
+    ret.append("SCRIPTING:EXECUTE:");
+    ret.append(call.replace("/", ":"));
+    return ret.toString();
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
index 6fa5d1a479df47c20d9d3a5d3a7134e1c0898ad1..fa51d5ff49c9693d2216fd5dc168873f850e6e25 100644
--- a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
+++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
@@ -35,6 +35,7 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.ProcessBuilder.Redirect;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -255,7 +256,12 @@ public class ServerSideScriptingCaller {
   }
 
   int callScript() throws IOException, InterruptedException, TimeoutException {
-    String[] effectiveCommandLine = injectAuthToken(getCommandLine());
+    String[] effectiveCommandLine;
+    if (authToken != null) {
+      effectiveCommandLine = injectAuthToken(getCommandLine());
+    } else {
+      effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length);
+    }
     effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]);
     final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine);
 
diff --git a/src/main/java/caosdb/server/terminal/CaosDBTerminal.java b/src/main/java/caosdb/server/terminal/CaosDBTerminal.java
index 8113d4e1e7c36adb517543b778d03ec9a0970598..c85495539d93d67af26b1b1ef4b62a35dd57ff48 100644
--- a/src/main/java/caosdb/server/terminal/CaosDBTerminal.java
+++ b/src/main/java/caosdb/server/terminal/CaosDBTerminal.java
@@ -30,6 +30,11 @@ import com.googlecode.lanterna.terminal.Terminal;
 import com.googlecode.lanterna.terminal.text.UnixTerminal;
 import java.nio.charset.Charset;
 
+/**
+ * @deprecated Soon to be removed
+ * @author Timm Fitschen (t.fitschen@indiscale.com)
+ */
+@Deprecated
 public class CaosDBTerminal extends Thread {
 
   public CaosDBTerminal() {
diff --git a/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java b/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java
index 76740a73c9439e4ea9e45479e57501c4c4e1a0d0..e864c20bcf040faf78dc54e24dbcdefc1a8be795 100644
--- a/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java
+++ b/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java
@@ -58,7 +58,7 @@ public class RetrieveUserRolesTransaction implements TransactionInterface {
   @Override
   public void execute() throws Exception {
     if (UserSources.isUserExisting(new Principal(this.realm, this.user))) {
-      this.roles = UserSources.resolve(this.realm, this.user);
+      this.roles = UserSources.resolveRoles(this.realm, this.user);
     } else {
       throw ServerMessages.ACCOUNT_DOES_NOT_EXIST;
     }
diff --git a/src/main/java/caosdb/server/transaction/Transaction.java b/src/main/java/caosdb/server/transaction/Transaction.java
index 28b8276e2b93699f2315fb754303194208bb37e5..d2c16438b5c071fec72b11b600604b46c4b2da30 100644
--- a/src/main/java/caosdb/server/transaction/Transaction.java
+++ b/src/main/java/caosdb/server/transaction/Transaction.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -23,7 +25,6 @@
 package caosdb.server.transaction;
 
 import caosdb.datetime.UTCDateTime;
-import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.Principal;
 import caosdb.server.database.DatabaseMonitor;
 import caosdb.server.database.access.Access;
@@ -227,14 +228,8 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra
   // TODO move to post-transaction job
   private void writeHistory() throws TransactionException, Message {
     if (logHistory()) {
-      String realm =
-          getTransactor().getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()
-              ? ""
-              : ((Principal) getTransactor().getPrincipal()).getRealm();
-      String username =
-          getTransactor().getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()
-              ? "anonymous"
-              : ((Principal) getTransactor().getPrincipal()).getUsername();
+      String realm = ((Principal) getTransactor().getPrincipal()).getRealm();
+      String username = ((Principal) getTransactor().getPrincipal()).getUsername();
       execute(
           new InsertTransactionHistory(
               getContainer(), this.getClass().getSimpleName(), realm, username, getTimestamp()),
diff --git a/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java b/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java
index 257e8c06cc9d9c9f24774e541cea611663f5ff0d..f11400c3826170de658fd9e5a634665f00e1cb44 100644
--- a/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java
+++ b/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java
@@ -68,7 +68,7 @@ public class UpdateUserRolesTransaction extends AccessControlTransaction {
   }
 
   public Element getUserRolesElement() {
-    final Set<String> resulting_roles = UserSources.resolve(this.realm, this.user);
+    final Set<String> resulting_roles = UserSources.resolveRoles(this.realm, this.user);
     final Element rolesElem = RetrieveUserRolesTransaction.getUserRolesElement(resulting_roles);
     if (!this.roles.equals(resulting_roles) && resulting_roles != null) {
       final Element warning = new Element("Warning");
diff --git a/src/main/java/caosdb/server/utils/FileUtils.java b/src/main/java/caosdb/server/utils/FileUtils.java
index 824646caab6983863acd7d35a276a1213b393235..2357b7217c0fa9b112dd6c9e45d5be64d36edefb 100644
--- a/src/main/java/caosdb/server/utils/FileUtils.java
+++ b/src/main/java/caosdb/server/utils/FileUtils.java
@@ -218,6 +218,13 @@ public class FileUtils {
     }
   }
 
+  /**
+   * @deprecated Soon to be removed.
+   * @throws IOException
+   * @throws InterruptedException
+   * @throws CaosDBException
+   */
+  @Deprecated
   public static void testChownScript() throws IOException, InterruptedException, CaosDBException {
     final String sudopw = CaosDBServer.getServerProperty(ServerProperties.KEY_SUDO_PASSWORD);
     final Process cmd =
diff --git a/src/main/java/caosdb/server/utils/Initialization.java b/src/main/java/caosdb/server/utils/Initialization.java
index e77922a417c1a2ef9542cb3dc9b8971ca10b332a..9f77fcf2992c0e7fb8de38ee334bebe8c93ef3d8 100644
--- a/src/main/java/caosdb/server/utils/Initialization.java
+++ b/src/main/java/caosdb/server/utils/Initialization.java
@@ -4,6 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2020 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020 Timm Fitschen <t.fitschen@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
@@ -26,7 +28,7 @@ import caosdb.server.database.DatabaseMonitor;
 import caosdb.server.database.access.Access;
 import caosdb.server.transaction.TransactionInterface;
 
-public final class Initialization implements TransactionInterface {
+public final class Initialization implements TransactionInterface, AutoCloseable {
 
   private Access access;
   private static final Initialization instance = new Initialization();
@@ -43,13 +45,14 @@ public final class Initialization implements TransactionInterface {
     return this.access;
   }
 
-  public final void release() {
+  @Override
+  public void execute() throws Exception {}
+
+  @Override
+  public void close() throws Exception {
     if (this.access != null) {
       this.access.release();
       this.access = null;
     }
   }
-
-  @Override
-  public void execute() throws Exception {}
 }
diff --git a/src/main/java/caosdb/server/utils/ServerMessages.java b/src/main/java/caosdb/server/utils/ServerMessages.java
index c2f3a302d270a3f16616c00b41ec9118862d4605..29983924246f5bc5987ffdbfeaf949d0946c7f16 100644
--- a/src/main/java/caosdb/server/utils/ServerMessages.java
+++ b/src/main/java/caosdb/server/utils/ServerMessages.java
@@ -133,8 +133,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.");
 
@@ -257,6 +255,9 @@ public class ServerMessages {
           0,
           "User has been activated. You can now log in with your username and password.");
 
+  public static final Message UNAUTHENTICATED =
+      new Message(MessageType.Error, 401, "Sign in, please.");
+
   public static final Message AUTHORIZATION_ERROR =
       new Message(MessageType.Error, 403, "You are not allowed to do this.");
 
diff --git a/src/main/java/caosdb/server/utils/Utils.java b/src/main/java/caosdb/server/utils/Utils.java
index 6c48906a40fd299379e91446516a9c0325200f81..085be51d0929ce76d10ed5e4e8c32696e0b48476 100644
--- a/src/main/java/caosdb/server/utils/Utils.java
+++ b/src/main/java/caosdb/server/utils/Utils.java
@@ -33,7 +33,6 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.text.DecimalFormat;
-import java.util.Random;
 import java.util.Scanner;
 import java.util.regex.Pattern;
 import org.apache.commons.codec.binary.Base32;
@@ -45,9 +44,6 @@ import org.jdom2.output.XMLOutputter;
 /** Utility functions. */
 public class Utils {
 
-  /** Random number generator initialized with the system time and used for generating UIDs. */
-  private static Random rand = new Random(System.currentTimeMillis());
-
   /** Secure random number generator, for secret random numbers. */
   private static final SecureRandom srand = new SecureRandom();
 
@@ -185,16 +181,14 @@ public class Utils {
    * @return The UID as a String.
    */
   public static String getUID() {
-    synchronized (rand) {
-      return Long.toHexString(rand.nextLong()) + Long.toHexString(rand.nextLong());
-    }
+    return Long.toHexString(srand.nextLong()) + Long.toHexString(srand.nextLong());
   }
 
   /**
    * Generate a secure filename (base32 letters and numbers).
    *
-   * <p>Very similar to getUID, but uses cryptographic random number instead, also also nicely
-   * formats the resulting string.
+   * <p>Very similar to getUID, but uses cryptographic random number instead, also nicely formats
+   * the resulting string.
    *
    * @param byteSize How many bytes of random bits shall be generated.
    * @return The filename as a String.
@@ -205,6 +199,7 @@ public class Utils {
     srand.nextBytes(bytes);
 
     // Encode to nice letters
+    // TODO use StringBuilder and iterate over filename.charArray
     String filename = (new Base32()).encodeToString(bytes);
     filename = filename.replaceAll("=+", "");
 
diff --git a/src/test/java/caosdb/server/Misc.java b/src/test/java/caosdb/server/Misc.java
index bcdc1729f008dbb1393fe238a3eb763c67d69e0f..2988bae6aa178ace45f41e03da5c54f08114e485 100644
--- a/src/test/java/caosdb/server/Misc.java
+++ b/src/test/java/caosdb/server/Misc.java
@@ -39,11 +39,7 @@ import java.io.ObjectOutputStream;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.config.Ini;
-import org.apache.shiro.config.IniSecurityManagerFactory;
-import org.apache.shiro.mgt.SecurityManager;
 import org.apache.shiro.subject.Subject;
-import org.apache.shiro.util.Factory;
 import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -293,12 +289,7 @@ public class Misc {
 
   @Test
   public void testShiro() {
-    Ini config = CaosDBServer.getShiroConfig();
-    final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
-
-    final SecurityManager securityManager = factory.getInstance();
-
-    SecurityUtils.setSecurityManager(securityManager);
+    CaosDBServer.initShiro();
 
     final Subject subject = SecurityUtils.getSubject();
 
diff --git a/src/test/java/caosdb/server/authentication/AuthTokenTest.java b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
index 9937dc60b1a33cc421a74661bed028ae5e24522a..9f344889051d8508125dc5f6417d40839c44bd3d 100644
--- a/src/test/java/caosdb/server/authentication/AuthTokenTest.java
+++ b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
@@ -22,13 +22,43 @@
  */
 package caosdb.server.authentication;
 
+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.Config;
 import caosdb.server.accessControl.OneTimeAuthenticationToken;
 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;
+import caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
+import caosdb.server.database.backend.interfaces.RetrieveUserImpl;
+import caosdb.server.resource.TestScriptingResource.RetrievePasswordValidator;
+import caosdb.server.resource.TestScriptingResource.RetrievePermissionRules;
+import caosdb.server.resource.TestScriptingResource.RetrieveRole;
+import caosdb.server.resource.TestScriptingResource.RetrieveUser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+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.subject.Subject;
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -39,62 +69,49 @@ public class AuthTokenTest {
     CaosDBServer.initServerProperties();
   }
 
+  @BeforeClass
+  public static void setupShiro() throws IOException {
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class);
+    BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class);
+    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class);
+    BackendTransaction.setImpl(
+        RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class);
+
+    CaosDBServer.initServerProperties();
+    CaosDBServer.initShiro();
+  }
+
+  @Before
+  public void reset() {
+    OneTimeAuthenticationToken.resetConfig();
+  }
+
   @Test
   public void testSessionToken() throws InterruptedException {
-    final String curry = "somecurry";
+    // Token 1 - wrong checksum, not expired
     final SessionToken t1 =
         new SessionToken(
             new Principal("somerealm", "someuser1"),
             System.currentTimeMillis(),
             60000,
             "345sdf56sdf",
-            curry,
-            "wrong checksum");
+            "wrong checksum",
+            null,
+            null);
     Assert.assertFalse(t1.isExpired());
     Assert.assertFalse(t1.isHashValid());
     Assert.assertFalse(t1.isValid());
 
-    final SessionToken t2 =
-        new SessionToken(
-            new Principal("somerealm", "someuser1"),
-            System.currentTimeMillis(),
-            60000,
-            "345sdf56sdf",
-            null,
-            "wrong checksum");
-    Assert.assertFalse(t2.isExpired());
-    Assert.assertFalse(t2.isHashValid());
-    Assert.assertFalse(t2.isValid());
-
+    // Token 3 - correct checksum, not expired
     final SessionToken t3 =
-        new SessionToken(
-            new Principal("somerealm", "someuser2"),
-            System.currentTimeMillis(),
-            60000,
-            "72723gsdg",
-            curry);
+        new SessionToken(new Principal("somerealm", "someuser2"), 60000, null, null);
     Assert.assertFalse(t3.isExpired());
     Assert.assertTrue(t3.isHashValid());
     Assert.assertTrue(t3.isValid());
 
-    final SessionToken t4 =
-        new SessionToken(
-            new Principal("somerealm", "someuser2"),
-            System.currentTimeMillis(),
-            60000,
-            "72723gsdg",
-            null);
-    Assert.assertFalse(t4.isExpired());
-    Assert.assertTrue(t4.isHashValid());
-    Assert.assertTrue(t4.isValid());
-
+    // Token 5 - correct checksum, soon to be expired
     final SessionToken t5 =
-        new SessionToken(
-            new Principal("somerealm", "someuser3"),
-            System.currentTimeMillis(),
-            2000,
-            "23sdfsg34",
-            curry);
+        new SessionToken(new Principal("somerealm", "someuser3"), 2000, null, null);
     Assert.assertFalse(t5.isExpired());
     Assert.assertTrue(t5.isHashValid());
     Assert.assertTrue(t5.isValid());
@@ -104,113 +121,331 @@ public class AuthTokenTest {
     Assert.assertTrue(t5.isHashValid());
     Assert.assertFalse(t5.isValid());
 
+    // Token 6 - correct checksum, immediately expired
     final SessionToken t6 =
-        new SessionToken(
-            new Principal("somerealm", "someuser3"),
-            System.currentTimeMillis(),
-            0,
-            "23sdfsg34",
-            null);
+        new SessionToken(new Principal("somerealm", "someuser3"), 0, null, null);
     Assert.assertTrue(t6.isExpired());
     Assert.assertTrue(t6.isHashValid());
     Assert.assertFalse(t6.isValid());
 
-    Assert.assertEquals(t1.toString(), SessionToken.parse(t1.toString(), curry).toString());
-    Assert.assertEquals(t2.toString(), SessionToken.parse(t2.toString(), curry).toString());
-    Assert.assertEquals(t3.toString(), SessionToken.parse(t3.toString(), curry).toString());
-    Assert.assertEquals(t4.toString(), SessionToken.parse(t4.toString(), curry).toString());
-    Assert.assertEquals(t5.toString(), SessionToken.parse(t5.toString(), curry).toString());
-    Assert.assertEquals(t6.toString(), SessionToken.parse(t6.toString(), curry).toString());
-    Assert.assertEquals(t1.toString(), SessionToken.parse(t1.toString(), null).toString());
-    Assert.assertEquals(t2.toString(), SessionToken.parse(t2.toString(), null).toString());
-    Assert.assertEquals(t3.toString(), SessionToken.parse(t3.toString(), null).toString());
-    Assert.assertEquals(t4.toString(), SessionToken.parse(t4.toString(), null).toString());
-    Assert.assertEquals(t5.toString(), SessionToken.parse(t5.toString(), null).toString());
-    Assert.assertEquals(t6.toString(), SessionToken.parse(t6.toString(), null).toString());
-
-    Assert.assertFalse(SessionToken.parse(t1.toString(), curry).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t2.toString(), null).isHashValid());
-    Assert.assertTrue(SessionToken.parse(t3.toString(), curry).isHashValid());
-    Assert.assertTrue(SessionToken.parse(t4.toString(), null).isHashValid());
-    Assert.assertTrue(SessionToken.parse(t5.toString(), curry).isHashValid());
-    Assert.assertTrue(SessionToken.parse(t6.toString(), null).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t1.toString(), null).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t2.toString(), curry).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t3.toString(), null).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t4.toString(), curry).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t5.toString(), null).isHashValid());
-    Assert.assertFalse(SessionToken.parse(t6.toString(), curry).isHashValid());
+    // All tokens can be successfully parsed back.
+    final SelfValidatingAuthenticationToken t1p = SessionToken.parse(t1.toString());
+    final SelfValidatingAuthenticationToken t3p = SessionToken.parse(t3.toString());
+    final SelfValidatingAuthenticationToken t5p = SessionToken.parse(t5.toString());
+    final SelfValidatingAuthenticationToken t6p = SessionToken.parse(t6.toString());
+    Assert.assertEquals(t1.toString(), t1p.toString());
+    Assert.assertEquals(t3.toString(), t3p.toString());
+    Assert.assertEquals(t5.toString(), t5p.toString());
+    Assert.assertEquals(t6.toString(), t6p.toString());
+
+    // ... and parsed tokens have the correct hash validation
+    Assert.assertFalse(t1p.isHashValid());
+    Assert.assertTrue(t3p.isHashValid());
+    Assert.assertTrue(t5p.isHashValid());
+    Assert.assertTrue(t6p.isHashValid());
 
     Assert.assertFalse(
         AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), null)
-            .isExpired());
-    Assert.assertTrue(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), null)
-            .isHashValid());
-    Assert.assertTrue(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), null)
-            .isValid());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), curry)
-            .isExpired());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), curry)
-            .isHashValid());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t4), curry)
-            .isValid());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), null)
-            .isExpired());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), null)
-            .isHashValid());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), null)
-            .isValid());
-    Assert.assertFalse(
-        AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), curry)
+                AuthenticationUtils.createSessionTokenCookie(t3))
             .isExpired());
     Assert.assertTrue(
         AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), curry)
+                AuthenticationUtils.createSessionTokenCookie(t3))
             .isHashValid());
     Assert.assertTrue(
         AuthenticationUtils.parseSessionTokenCookie(
-                AuthenticationUtils.createSessionTokenCookie(t3), curry)
+                AuthenticationUtils.createSessionTokenCookie(t3))
             .isValid());
+
+    // TODO parse invalid tokens
   }
 
   @Test
-  public void testOneTimeToken() {
-    final String curry = null;
+  public void testOneTimeTokenSerialization() {
     final OneTimeAuthenticationToken t1 =
         new OneTimeAuthenticationToken(
             new Principal("somerealm", "someuser"),
-            System.currentTimeMillis(),
-            60000L,
-            "sdfh37456sd",
-            curry,
-            new String[] {""});
-    System.err.println(t1.toString());
+            60000,
+            new String[] {"permissions"},
+            new String[] {"roles"},
+            1L,
+            3000L);
+    Assert.assertEquals(1L, t1.getMaxReplays());
     Assert.assertFalse(t1.isExpired());
     Assert.assertTrue(t1.isHashValid());
     Assert.assertTrue(t1.isValid());
 
+    String serialized = t1.toString();
+    OneTimeAuthenticationToken parsed =
+        (OneTimeAuthenticationToken) OneTimeAuthenticationToken.parse(serialized);
+
+    Assert.assertEquals(t1, parsed);
+    Assert.assertEquals(serialized, parsed.toString());
+
+    Assert.assertEquals(1L, parsed.getMaxReplays());
+    Assert.assertFalse(parsed.isExpired());
+    Assert.assertTrue(parsed.isHashValid());
+    Assert.assertTrue(parsed.isValid());
+  }
+
+  @Test(expected = AuthenticationException.class)
+  public void testOneTimeTokenConsume() {
+    final OneTimeAuthenticationToken t1 =
+        new OneTimeAuthenticationToken(
+            new Principal("somerealm", "someuser"),
+            60000,
+            new String[] {"permissions"},
+            new String[] {"roles"},
+            3L,
+            3000L);
+    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 OneTimeAuthenticationToken t1 =
+        new OneTimeAuthenticationToken(
+            new Principal("somerealm", "someuser"),
+            60000,
+            new String[] {"permissions"},
+            new String[] {"roles"},
+            3L,
+            3000L);
+    Assert.assertFalse(t1.isExpired());
+    Assert.assertTrue(t1.isHashValid());
+    Assert.assertTrue(t1.isValid());
+
+    String serialized = t1.toString();
+    try {
+      SelfValidatingAuthenticationToken parsed1 = OneTimeAuthenticationToken.parse(serialized);
+      Assert.assertTrue(parsed1.isValid());
+      SelfValidatingAuthenticationToken parsed2 = OneTimeAuthenticationToken.parse(serialized);
+      Assert.assertTrue(parsed2.isValid());
+      SelfValidatingAuthenticationToken parsed3 = OneTimeAuthenticationToken.parse(serialized);
+      Assert.assertTrue(parsed3.isValid());
+    } catch (AuthenticationException e) {
+      Assert.fail(e.getMessage());
+    }
+
+    // throws
+    OneTimeAuthenticationToken.parse(serialized);
+    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_ONE_TIME_TOKEN_EXPIRES_MS)),
+        config.getExpiresAfter());
+    Assert.assertEquals(1, config.getMaxReplays());
+    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
+  public void testOneTimeTokenConfigBasic() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("purpose: test purpose 1\n");
+    testYaml.append("roles: [ role1, \"role2\"]\n");
+    testYaml.append("expiresAfterSeconds: 10\n");
+    testYaml.append("maxReplays: 3\n");
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    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.getExpiresAfter());
+    Assert.assertEquals(3, config.getMaxReplays());
+
+    Assert.assertArrayEquals(
+        "permissions parsed",
+        new String[] {"permission1", "permission2", "permission3", "permission with white space"},
+        config.getPermissions());
+    Assert.assertArrayEquals("roles parsed", new String[] {"role1", "role2"}, config.getRoles());
+  }
+
+  @Test
+  public void testOneTimeTokenConfigNoRoles() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("purpose: no roles test\n");
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Config config = map.get("no roles test");
+
+    Assert.assertArrayEquals("empty roles array parsed", new String[] {}, config.getRoles());
+  }
+
+  @Test
+  public void testOneTimeTokenConfigNoPurpose() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Assert.assertEquals(map.size(), 0);
+  }
+
+  @Test
+  public void testOneTimeTokenConfigMulti() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- purpose: purpose 1\n");
+    testYaml.append("- purpose: purpose 2\n");
+    testYaml.append("- purpose: purpose 3\n");
+    testYaml.append("- purpose: purpose 4\n");
+
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Assert.assertEquals("four items", 4, map.size());
+    Assert.assertTrue(map.get("purpose 2") instanceof Config);
+  }
+
+  @Test
+  public void testOneTimeTokenConfigOutputFile() throws Exception {
+    File tempFile = File.createTempFile("authtoken", "json");
+    tempFile.deleteOnExit();
+
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- output:\n");
+    testYaml.append("    file: " + tempFile.getAbsolutePath() + "\n");
+    testYaml.append("  permissions: [ permission1 ]\n");
+
+    // write the token
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+    Assert.assertTrue(tempFile.exists());
+    try (BufferedReader reader = new BufferedReader(new FileReader(tempFile))) {
+      OneTimeAuthenticationToken token =
+          (OneTimeAuthenticationToken) SelfValidatingAuthenticationToken.parse(reader.readLine());
+      assertEquals("Token has anonymous username", "anonymous", token.getPrincipal().getUsername());
+      assertEquals(
+          "Token has anonymous realm",
+          OneTimeAuthenticationToken.REALM_NAME,
+          token.getPrincipal().getRealm());
+      assertArrayEquals(
+          "Permissions array has been written and read",
+          new String[] {"permission1"},
+          token.getPermissions().toArray());
+    }
+  }
+
+  @Test
+  public void testOneTimeTokenForAnonymous() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("purpose: for anonymous\n");
+    testYaml.append("roles: [ role1 ]\n");
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+
+    OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
+
+    Subject anonymous = SecurityUtils.getSubject();
+    anonymous.login(AnonymousAuthenticationToken.getInstance());
+
+    OneTimeAuthenticationToken token =
+        OneTimeAuthenticationToken.generateForPurpose("for anonymous", anonymous);
+    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);
+
+    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);
+  }
+
+  @Test
+  public void testIntInConfigYaml() throws IOException {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- expiresAfterSeconds: 1000\n");
+    testYaml.append("  replayTimeout: 1000\n");
+
+    ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+    ObjectReader reader = mapper.readerFor(Config.class);
+    Config config =
+        (Config) reader.readValues(new CharSequenceInputStream(testYaml, "utf-8")).next();
+
+    assertEquals(1000000, config.getExpiresAfter());
+    assertEquals(1000, config.getReplayTimeout());
+  }
+
+  @Test
+  public void testLongInConfigYaml() throws IOException {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- expiresAfter: 9223372036854775000\n");
+    testYaml.append("  replayTimeoutSeconds: 922337203685477\n");
+
+    ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+    ObjectReader reader = mapper.readerFor(Config.class);
+    Config config =
+        (Config) reader.readValues(new CharSequenceInputStream(testYaml, "utf-8")).next();
+
+    assertEquals(9223372036854775000L, config.getExpiresAfter());
+    assertEquals(922337203685477000L, config.getReplayTimeout());
   }
 }
diff --git a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
deleted file mode 100644
index 08a0e412d8562779cb54574f94ddca932ff54777..0000000000000000000000000000000000000000
--- a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
+++ /dev/null
@@ -1,277 +0,0 @@
-package caosdb.server.jobs.extension;
-
-import static org.junit.Assert.assertEquals;
-
-import caosdb.server.entity.container.TransactionContainer;
-import caosdb.server.jobs.core.Mode;
-import caosdb.server.transaction.Transaction;
-import caosdb.server.utils.EntityStatus;
-import java.util.Collection;
-import java.util.List;
-import java.util.concurrent.Callable;
-import org.apache.shiro.authc.AuthenticationException;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authz.AuthorizationException;
-import org.apache.shiro.authz.Permission;
-import org.apache.shiro.session.Session;
-import org.apache.shiro.subject.ExecutionException;
-import org.apache.shiro.subject.PrincipalCollection;
-import org.apache.shiro.subject.Subject;
-import org.junit.Test;
-
-public class TestAWIBoxLoan {
-
-  @Test
-  public void testNonAnonymousUser() {
-    TransactionContainer container = new TransactionContainer();
-    AWIBoxLoan j =
-        new AWIBoxLoan() {
-
-          @Override
-          protected Subject getUser() {
-            return new Subject() {
-
-              @Override
-              public void runAs(PrincipalCollection principals)
-                  throws NullPointerException, IllegalStateException {}
-
-              @Override
-              public PrincipalCollection releaseRunAs() {
-                return null;
-              }
-
-              @Override
-              public void logout() {}
-
-              @Override
-              public void login(AuthenticationToken token) throws AuthenticationException {}
-
-              @Override
-              public boolean isRunAs() {
-                return false;
-              }
-
-              @Override
-              public boolean isRemembered() {
-                return false;
-              }
-
-              @Override
-              public boolean isPermittedAll(Collection<Permission> permissions) {
-                return false;
-              }
-
-              @Override
-              public boolean isPermittedAll(String... permissions) {
-                return false;
-              }
-
-              @Override
-              public boolean[] isPermitted(List<Permission> permissions) {
-                return null;
-              }
-
-              @Override
-              public boolean[] isPermitted(String... permissions) {
-                return null;
-              }
-
-              @Override
-              public boolean isPermitted(Permission permission) {
-
-                return false;
-              }
-
-              @Override
-              public boolean isPermitted(String permission) {
-
-                return false;
-              }
-
-              @Override
-              public boolean isAuthenticated() {
-
-                return false;
-              }
-
-              @Override
-              public boolean[] hasRoles(List<String> roleIdentifiers) {
-
-                return null;
-              }
-
-              @Override
-              public boolean hasRole(String roleIdentifier) {
-
-                return false;
-              }
-
-              @Override
-              public boolean hasAllRoles(Collection<String> roleIdentifiers) {
-
-                return false;
-              }
-
-              @Override
-              public Session getSession(boolean create) {
-                return null;
-              }
-
-              @Override
-              public Session getSession() {
-                return null;
-              }
-
-              @Override
-              public PrincipalCollection getPrincipals() {
-                return null;
-              }
-
-              @Override
-              public Object getPrincipal() {
-                return null;
-              }
-
-              @Override
-              public PrincipalCollection getPreviousPrincipals() {
-                return null;
-              }
-
-              @Override
-              public void execute(Runnable runnable) {}
-
-              @Override
-              public <V> V execute(Callable<V> callable) throws ExecutionException {
-                return null;
-              }
-
-              @Override
-              public void checkRoles(String... roleIdentifiers) throws AuthorizationException {}
-
-              @Override
-              public void checkRoles(Collection<String> roleIdentifiers)
-                  throws AuthorizationException {}
-
-              @Override
-              public void checkRole(String roleIdentifier) throws AuthorizationException {}
-
-              @Override
-              public void checkPermissions(Collection<Permission> permissions)
-                  throws AuthorizationException {}
-
-              @Override
-              public void checkPermissions(String... permissions) throws AuthorizationException {}
-
-              @Override
-              public void checkPermission(Permission permission) throws AuthorizationException {}
-
-              @Override
-              public void checkPermission(String permission) throws AuthorizationException {}
-
-              @Override
-              public Runnable associateWith(Runnable runnable) {
-                return null;
-              }
-
-              @Override
-              public <V> Callable<V> associateWith(Callable<V> callable) {
-                return null;
-              }
-            };
-          }
-        };
-    Transaction<TransactionContainer> t =
-        new Transaction<TransactionContainer>(container, null) {
-
-          @Override
-          protected void transaction() throws Exception {}
-
-          @Override
-          protected void preTransaction() throws InterruptedException {}
-
-          @Override
-          protected void preCheck() throws InterruptedException, Exception {}
-
-          @Override
-          protected void postTransaction() throws Exception {}
-
-          @Override
-          protected void postCheck() {}
-
-          @Override
-          public boolean logHistory() {
-            return false;
-          }
-
-          @Override
-          protected void init() throws Exception {}
-
-          @Override
-          protected void cleanUp() {}
-        };
-
-    j.init(Mode.MUST, null, t);
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-
-    // non-anonymous user
-    j.run();
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-  }
-
-  @Test
-  public void testAnonymousUserUnqualified() {
-    TransactionContainer container = new TransactionContainer();
-    AWIBoxLoan j =
-        new AWIBoxLoan() {
-
-          @Override
-          protected Subject getUser() {
-            return null;
-          }
-
-          @Override
-          boolean isAnonymous() {
-            return true;
-          }
-        };
-    Transaction<TransactionContainer> t =
-        new Transaction<TransactionContainer>(container, null) {
-
-          @Override
-          protected void transaction() throws Exception {}
-
-          @Override
-          protected void preTransaction() throws InterruptedException {}
-
-          @Override
-          protected void preCheck() throws InterruptedException, Exception {}
-
-          @Override
-          protected void postTransaction() throws Exception {}
-
-          @Override
-          protected void postCheck() {}
-
-          @Override
-          public boolean logHistory() {
-            return false;
-          }
-
-          @Override
-          protected void init() throws Exception {}
-
-          @Override
-          protected void cleanUp() {}
-        };
-
-    j.init(Mode.MUST, null, t);
-    assertEquals(0, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
-
-    j.run();
-    assertEquals(2, j.getContainer().getMessages().size());
-    assertEquals(EntityStatus.UNQUALIFIED, j.getContainer().getStatus());
-  }
-}
diff --git a/src/test/java/caosdb/server/permissions/EntityACLTest.java b/src/test/java/caosdb/server/permissions/EntityACLTest.java
index 13ebb6279546606c8e796672db09a7e99ffea588..6aae2257e5623d072c5f60e5991a5cb0248ecf74 100644
--- a/src/test/java/caosdb/server/permissions/EntityACLTest.java
+++ b/src/test/java/caosdb/server/permissions/EntityACLTest.java
@@ -23,14 +23,29 @@
 package caosdb.server.permissions;
 
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import caosdb.server.CaosDBServer;
+import caosdb.server.accessControl.AnonymousAuthenticationToken;
+import caosdb.server.accessControl.AuthenticationUtils;
+import caosdb.server.accessControl.Config;
+import caosdb.server.accessControl.OneTimeAuthenticationToken;
+import caosdb.server.accessControl.Role;
+import caosdb.server.database.BackendTransaction;
+import caosdb.server.database.access.Access;
+import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
+import caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
+import caosdb.server.database.exceptions.TransactionException;
+import caosdb.server.database.misc.TransactionBenchmark;
 import caosdb.server.resource.AbstractCaosDBServerResource;
 import caosdb.server.resource.AbstractCaosDBServerResource.XMLParser;
 import caosdb.server.utils.Utils;
 import java.io.IOException;
 import java.util.BitSet;
+import java.util.HashSet;
 import java.util.LinkedList;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
 import org.jdom2.Element;
 import org.jdom2.JDOMException;
 import org.junit.Assert;
@@ -47,10 +62,53 @@ public class EntityACLTest {
     return value;
   }
 
+  /** a no-op mock-up which resolved all rules to an empty set of permissions. */
+  public static class RetrievePermissionRulesMockup implements RetrievePermissionRulesImpl {
+
+    public RetrievePermissionRulesMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException {
+      return new HashSet<>();
+    }
+  }
+
+  /** a mock-up which returns null */
+  public static class RetrieveRoleMockup implements RetrieveRoleImpl {
+
+    public RetrieveRoleMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public Role retrieve(String role) throws TransactionException {
+      return null;
+    }
+  }
+
   @BeforeClass
   public static void init() throws IOException {
     CaosDBServer.initServerProperties();
+    CaosDBServer.initShiro();
     assertNotNull(EntityACL.GLOBAL_PERMISSIONS);
+
+    BackendTransaction.setImpl(
+        RetrievePermissionRulesImpl.class, RetrievePermissionRulesMockup.class);
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
   }
 
   @Test
@@ -172,35 +230,31 @@ public class EntityACLTest {
     Assert.assertEquals(convert(EntityACL.convert(EntityACL.OWNER_BITSET).get(1, 32)), 0);
   }
 
-  // @Test
-  // public void testDeserialize() {
-  // Assert.assertTrue(EntityACL.deserialize("{}") instanceof EntityACL);
-  // Assert.assertTrue(EntityACL.deserialize("{tf:134}") instanceof
-  // EntityACL);
-  // Assert.assertTrue(EntityACL.deserialize("{tf:6343;bla:884}") instanceof
-  // EntityACL);
-  // Assert.assertTrue(EntityACL.deserialize("{tf:-2835;bla:884}") instanceof
-  // EntityACL);
-  // Assert.assertTrue(EntityACL.deserialize("{?OWNER?:526;tahsdh   : -235;}")
-  // instanceof EntityACL);
-  // Assert.assertTrue(EntityACL.deserialize("{asdf:2345;}") instanceof
-  // EntityACL);
-  // Assert.assertTrue(raisesIllegalArguementException("{"));
-  // Assert.assertTrue(raisesIllegalArguementException("}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf:}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf:;}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{:234}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{:234;}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf:tf;}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf: +5259;}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf;}"));
-  // Assert.assertTrue(raisesIllegalArguementException("{tf:123223727356235782735235;}"));
-  // }
-
-  public boolean raisesIllegalArguementException(final String input) {
+  @Test
+  public void testDeserialize() {
+    Assert.assertTrue(EntityACL.deserialize("{}") instanceof EntityACL);
+    Assert.assertTrue(EntityACL.deserialize("{\"tf\":134}") instanceof EntityACL);
+    Assert.assertTrue(EntityACL.deserialize("{\"tf\":6343,\"bla\":884}") instanceof EntityACL);
+    Assert.assertTrue(EntityACL.deserialize("{\"tf\":-2835,\"bla\":884}") instanceof EntityACL);
+    Assert.assertTrue(
+        EntityACL.deserialize("{\"?OWNER?\":526,\"tahsdh   \": -235}") instanceof EntityACL);
+    Assert.assertTrue(EntityACL.deserialize("{\"asdf\":2345}") instanceof EntityACL);
+    Assert.assertTrue(raisesIllegalStateException("{"));
+    Assert.assertTrue(raisesIllegalStateException("}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf:}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf:;}"));
+    Assert.assertTrue(raisesIllegalStateException("{:234}"));
+    Assert.assertTrue(raisesIllegalStateException("{:234;}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf:tf;}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf: +5259;}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf;}"));
+    Assert.assertTrue(raisesIllegalStateException("{tf:123223727356235782735235;}"));
+  }
+
+  public boolean raisesIllegalStateException(final String input) {
     try {
       EntityACL.deserialize(input);
-    } catch (final IllegalArgumentException e) {
+    } catch (final IllegalStateException e) {
       return true;
     }
     return false;
@@ -212,134 +266,143 @@ public class EntityACLTest {
     return parser.parse(Utils.String2InputStream(s)).getRootElement();
   }
 
-  // @Test
-  // public void testParseFromElement() throws JDOMException, IOException {
-  // Assert.assertEquals("{}",
-  // EntityACL.serialize(EntityACL.parseFromElement(stringToJdom("<ACL></ACL>"))));
-  // Assert.assertEquals("{}", EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Grant></Grant></ACL>"))));
-  // Assert.assertEquals("{}", EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Deny></Deny></ACL>"))));
-  // Assert.assertEquals("{}", EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Grant role='bla'></Grant></ACL>"))));
-  // Assert.assertEquals("{}", EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Deny role='bla'></Deny></ACL>"))));
-  // Assert.assertEquals(
-  // "{bla:2;}",
-  // EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission name='DELETE'
-  // /></Grant></ACL>"))));
-  // Assert.assertEquals(
-  // "{bla:" + (Long.MIN_VALUE + 2) + ";}",
-  // EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Deny role='bla'><Permission name='DELETE'
+  @Test
+  public void testEntityACLForAnonymous() {
+    Subject anonymous = SecurityUtils.getSubject();
+    anonymous.login(AnonymousAuthenticationToken.getInstance());
+    assertTrue(AuthenticationUtils.isAnonymous(anonymous));
+    EntityACL acl = EntityACL.getOwnerACLFor(anonymous);
+    assertNotNull(acl);
+    assertTrue(acl.getOwners().isEmpty());
+  }
+
+  //   @Test
+  //   public void testParseFromElement() throws JDOMException, IOException {
+  //   Assert.assertEquals("[]",
+  //   EntityACL.serialize(EntityACL.parseFromElement(stringToJdom("<ACL></ACL>"))));
+  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Grant></Grant></ACL>"))));
+  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Deny></Deny></ACL>"))));
+  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'></Grant></ACL>"))));
+  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Deny role='bla'></Deny></ACL>"))));
+  //   Assert.assertEquals(
+  //   "{bla:2;}",
+  //   EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission
+  // name='DELETE'/></Grant></ACL>"))));
+  //   Assert.assertEquals(
+  //   "{bla:" + (Long.MIN_VALUE + 2) + ";}",
+  //   EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Deny role='bla'><Permission name='DELETE'
   // /></Deny></ACL>"))));
-  // Assert.assertEquals(
-  // "{bla:32;}",
-  // EntityACL.serialize(EntityACL
-  // .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission name='RETRIEVE:ACL'
+  //   Assert.assertEquals(
+  //   "{bla:32;}",
+  //   EntityACL.serialize(EntityACL
+  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission name='RETRIEVE:ACL'
   // /></Grant></ACL>"))));
-  // }
-
-  // @Test
-  // public void testFactory() {
-  // final EntityACLFactory f = new EntityACLFactory();
-  // f.grant("user1", "UPDATE:NAME");
-  // Assert.assertTrue((f.create().isPermitted("user1",
-  // EntityPermission.UPDATE_NAME)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.UPDATE_NAME)));
-  // f.grant("user2", "DELETE");
-  // Assert.assertFalse((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertTrue((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // f.deny("user2", 1);
-  // f.deny("user1", 1);
-  // Assert.assertFalse((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // f.grant("user1", true, 1);
-  // Assert.assertTrue((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // f.deny("user2", true, 1);
-  // Assert.assertTrue((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // f.grant("user2", true, 1);
-  // Assert.assertTrue((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // f.deny("user1", true, 1);
-  // Assert.assertFalse((f.create().isPermitted("user1",
-  // EntityPermission.DELETE)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.DELETE)));
-  // Assert.assertTrue((f.create().isPermitted("user1",
-  // EntityPermission.UPDATE_NAME)));
-  // Assert.assertFalse((f.create().isPermitted("user2",
-  // EntityPermission.UPDATE_NAME)));
-  // }
-
-  // @Test
-  // public void niceFactoryStuff() {
-  // final EntityACLFactory f = new EntityACLFactory();
-  // f.grant("user1", "*");
-  // final EntityACL acl1 = f.create();
-  // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL));
-  // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE));
-  // Assert.assertTrue(acl1.isPermitted("user1",
-  // EntityPermission.RETRIEVE_ENTITY));
-  // Assert.assertTrue(acl1.isPermitted("user1",
-  // EntityPermission.UPDATE_DATA_TYPE));
-  // Assert.assertTrue(acl1.isPermitted("user1",
-  // EntityPermission.USE_AS_PROPERTY));
+  //   }
+
+  @Test
+  public void testFactory() {
+    final EntityACLFactory f = new EntityACLFactory();
+
+    caosdb.server.permissions.Role role1 = caosdb.server.permissions.Role.create("role1");
+    Config config1 = new Config();
+    config1.setRoles(new String[] {role1.toString()});
+    OneTimeAuthenticationToken token1 = OneTimeAuthenticationToken.generate(config1);
+    Subject user1 = SecurityUtils.getSecurityManager().createSubject(null);
+    user1.login(token1);
+
+    caosdb.server.permissions.Role role2 = caosdb.server.permissions.Role.create("role2");
+    Config config2 = new Config();
+    config2.setRoles(new String[] {role2.toString()});
+    OneTimeAuthenticationToken token2 = OneTimeAuthenticationToken.generate(config2);
+    Subject user2 = SecurityUtils.getSecurityManager().createSubject(null);
+    user2.login(token2);
+
+    f.grant(role1, "UPDATE:NAME");
+    Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.UPDATE_NAME)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME)));
+    f.grant(role2, "DELETE");
+    Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertTrue((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    f.deny(role2, 1);
+    f.deny(role1, 1);
+    Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    f.grant(role1, true, 1);
+    Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    f.deny(role2, true, 1);
+    Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    f.grant(role2, true, 1);
+    Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    f.deny(role1, true, 1);
+    Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE)));
+    Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.UPDATE_NAME)));
+    Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME)));
+  }
+
+  //   @Test
+  //   public void niceFactoryStuff() {
+  //   final EntityACLFactory f = new EntityACLFactory();
+  //   f.grant("user1", "*");
+  //   final EntityACL acl1 = f.create();
+  //   Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL));
+  //   Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE));
+  //   Assert.assertTrue(acl1.isPermitted("user1",
+  //   EntityPermission.RETRIEVE_ENTITY));
+  //   Assert.assertTrue(acl1.isPermitted("user1",
+  //   EntityPermission.UPDATE_DATA_TYPE));
+  //   Assert.assertTrue(acl1.isPermitted("user1",
+  //   EntityPermission.USE_AS_PROPERTY));
+  //
+  //   f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*",
+  //   "USE:*");
+  //   f.grant("user2", "EDIT:ACL");
+  //   final EntityACL acl2 = f.create();
+  //   Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL));
+  //   Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE));
+  //   Assert.assertTrue(acl2.isPermitted("user2",
+  //   EntityPermission.RETRIEVE_ENTITY));
+  //   Assert.assertTrue(acl2.isPermitted("user2",
+  //   EntityPermission.UPDATE_DATA_TYPE));
+  //   Assert.assertTrue(acl2.isPermitted("user2",
+  //   EntityPermission.USE_AS_PROPERTY));
   //
-  // f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*",
-  // "USE:*");
-  // f.grant("user2", "EDIT:ACL");
-  // final EntityACL acl2 = f.create();
-  // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL));
-  // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE));
-  // Assert.assertTrue(acl2.isPermitted("user2",
-  // EntityPermission.RETRIEVE_ENTITY));
-  // Assert.assertTrue(acl2.isPermitted("user2",
-  // EntityPermission.UPDATE_DATA_TYPE));
-  // Assert.assertTrue(acl2.isPermitted("user2",
-  // EntityPermission.USE_AS_PROPERTY));
+  //   }
   //
-  // }
-
-  // @Test
-  // public void testDeny() {
-  // EntityACLFactory f = new EntityACLFactory();
-  // f.deny("test", "DELETE");
-  // Assert.assertFalse(f.create().isPermitted("test",
-  // EntityPermission.DELETE));
+  //   @Test
+  //   public void testDeny() {
+  //   EntityACLFactory f = new EntityACLFactory();
+  //   f.deny("test", "DELETE");
+  //   Assert.assertFalse(f.create().isPermitted("test",
+  //   EntityPermission.DELETE));
   //
-  // System.out.println(Utils.element2String(f.create().toElement()));
+  //   System.out.println(Utils.element2String(f.create().toElement()));
   //
-  // System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement()));
+  //   System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement()));
   //
-  // f.grant("test", "USE:*");
-  // Assert.assertFalse(f.create().isPermitted("test",
-  // EntityPermission.DELETE));
+  //   f.grant("test", "USE:*");
+  //   Assert.assertFalse(f.create().isPermitted("test",
+  //   EntityPermission.DELETE));
   //
-  // System.out.println(Utils.element2String(f.create().toElement()));
+  //   System.out.println(Utils.element2String(f.create().toElement()));
   //
-  // f = new EntityACLFactory();
-  // f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*");
-  // f.deny(EntityACL.OTHER_ROLE, "DELETE");
-  // final EntityACL a = f.create();
+  //   f = new EntityACLFactory();
+  //   f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*");
+  //   f.deny(EntityACL.OTHER_ROLE, "DELETE");
+  //   final EntityACL a = f.create();
   //
-  // System.out.println(Utils.element2String(a.toElement()));
+  //   System.out.println(Utils.element2String(a.toElement()));
   //
-  // System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement()));
-  // }
+  //   System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement()));
+  //   }
 
 }
diff --git a/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java b/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java
index 81baabd4797842f1cbbc61f990d6ea580553de85..59f0a1d5812b1127ea0ff4514520bc30039af5cd 100644
--- a/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java
+++ b/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java
@@ -8,12 +8,21 @@ import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
 import caosdb.server.accessControl.AnonymousAuthenticationToken;
 import caosdb.server.accessControl.AnonymousRealm;
+import caosdb.server.accessControl.Role;
+import caosdb.server.database.BackendTransaction;
+import caosdb.server.database.access.Access;
 import caosdb.server.database.backend.implementation.MySQL.ConnectionException;
+import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
+import caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
+import caosdb.server.database.exceptions.TransactionException;
+import caosdb.server.database.misc.TransactionBenchmark;
+import caosdb.server.permissions.PermissionRule;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.security.NoSuchAlgorithmException;
 import java.sql.SQLException;
+import java.util.HashSet;
 import org.apache.shiro.mgt.DefaultSecurityManager;
 import org.apache.shiro.subject.Subject;
 import org.apache.shiro.subject.support.DelegatingSubject;
@@ -29,9 +38,51 @@ public class TestAbstractCaosDBServerResource {
 
   @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
 
+  /** a no-op mock-up which resolved all rules to an empty set of permissions. */
+  public static class RetrievePermissionRulesMockup implements RetrievePermissionRulesImpl {
+
+    public RetrievePermissionRulesMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException {
+      return new HashSet<>();
+    }
+  }
+
+  /** a no-op mock-up which returns null */
+  public static class RetrieveRoleMockup implements RetrieveRoleImpl {
+
+    public RetrieveRoleMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public Role retrieve(String role) throws TransactionException {
+      return null;
+    }
+  }
+
   @BeforeClass
   public static void initServerProperties() throws IOException {
     CaosDBServer.initServerProperties();
+
+    BackendTransaction.setImpl(
+        RetrievePermissionRulesImpl.class, RetrievePermissionRulesMockup.class);
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
   }
 
   @Test
diff --git a/src/test/java/caosdb/server/resource/TestScriptingResource.java b/src/test/java/caosdb/server/resource/TestScriptingResource.java
index 2edfb82b53b56324996293a469fadf807264d86c..3280ae3af5a62174006b504408d7c3bde8a2632e 100644
--- a/src/test/java/caosdb/server/resource/TestScriptingResource.java
+++ b/src/test/java/caosdb/server/resource/TestScriptingResource.java
@@ -23,14 +23,13 @@
 package caosdb.server.resource;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import caosdb.server.CaosDBServer;
-import caosdb.server.accessControl.AuthenticationUtils;
+import caosdb.server.accessControl.AnonymousAuthenticationToken;
 import caosdb.server.accessControl.CredentialsValidator;
 import caosdb.server.accessControl.Principal;
 import caosdb.server.accessControl.Role;
-import caosdb.server.accessControl.SessionToken;
+import caosdb.server.accessControl.UserSources;
 import caosdb.server.database.BackendTransaction;
 import caosdb.server.database.access.Access;
 import caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl;
@@ -42,16 +41,16 @@ import caosdb.server.database.misc.TransactionBenchmark;
 import caosdb.server.database.proto.ProtoUser;
 import caosdb.server.entity.Message;
 import caosdb.server.permissions.PermissionRule;
+import caosdb.server.scripting.ScriptingPermissions;
+import caosdb.server.scripting.ServerSideScriptingCaller;
 import java.io.IOException;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.config.Ini;
-import org.apache.shiro.config.IniSecurityManagerFactory;
-import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.authz.permission.WildcardPermission;
 import org.apache.shiro.subject.Subject;
-import org.apache.shiro.util.Factory;
+import org.jdom2.Element;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.restlet.Request;
@@ -93,7 +92,13 @@ public class TestScriptingResource {
 
     @Override
     public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException {
-      return new HashSet<>();
+      HashSet<PermissionRule> result = new HashSet<>();
+      result.add(
+          new PermissionRule(
+              true,
+              false,
+              new WildcardPermission(ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok"))));
+      return result;
     }
 
     @Override
@@ -149,19 +154,16 @@ public class TestScriptingResource {
 
   @BeforeClass
   public static void setupShiro() throws IOException {
+    CaosDBServer.initServerProperties();
+    CaosDBServer.initShiro();
+
     BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class);
     BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class);
     BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class);
     BackendTransaction.setImpl(
         RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class);
 
-    CaosDBServer.initServerProperties();
-    Ini config = CaosDBServer.getShiroConfig();
-    final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config);
-
-    final SecurityManager securityManager = factory.getInstance();
-
-    SecurityUtils.setSecurityManager(securityManager);
+    UserSources.getDefaultRealm();
   }
 
   ScriptingResource resource =
@@ -173,23 +175,25 @@ public class TestScriptingResource {
             java.util.List<caosdb.server.entity.FileProperties> files,
             Object authToken)
             throws Message {
+          if (invokation.get(0).equals("anonymous_ok")) {
+            return 0;
+          }
           return -1;
         };
 
         @Override
-        public Object generateAuthToken() {
+        public Element generateRootElement(ServerSideScriptingCaller caller) {
+          return new Element("OK");
+        };
+
+        @Override
+        public Object generateAuthToken(String purpose) {
           return "";
         }
       };
 
   @Test
   public void testUnsupportedMediaType() {
-    Subject user = SecurityUtils.getSubject();
-    if (user.isAuthenticated()) {
-      user.logout();
-    }
-    SessionToken t = SessionToken.generate(new Principal("CaosDB", "user"), null);
-    user.login(t);
     Representation entity = new StringRepresentation("asdf");
     entity.setMediaType(MediaType.TEXT_ALL);
     Request request = new Request(Method.POST, "../test", entity);
@@ -201,12 +205,11 @@ public class TestScriptingResource {
   }
 
   @Test
-  public void testAnonymous() {
+  public void testAnonymousWithOutPermission() {
     Subject user = SecurityUtils.getSubject();
-    user.login(AuthenticationUtils.ANONYMOUS_USER);
-    assertTrue(resource.isAnonymous());
-    Representation entity = new StringRepresentation("asdf");
-    entity.setMediaType(MediaType.TEXT_ALL);
+    user.login(AnonymousAuthenticationToken.getInstance());
+    Form form = new Form("call=anonymous_no_permission");
+    Representation entity = form.getWebRepresentation();
     Request request = new Request(Method.POST, "../test", entity);
     request.setRootRef(new Reference("bla"));
     request.getAttributes().put("SRID", "asdf1234");
@@ -218,6 +221,23 @@ public class TestScriptingResource {
     assertEquals(Status.CLIENT_ERROR_FORBIDDEN, resource.getResponse().getStatus());
   }
 
+  @Test
+  public void testAnonymousWithPermission() {
+    Subject user = SecurityUtils.getSubject();
+    user.login(AnonymousAuthenticationToken.getInstance());
+    Form form = new Form("call=anonymous_ok");
+    Representation entity = form.getWebRepresentation();
+    Request request = new Request(Method.POST, "../test", entity);
+    request.setRootRef(new Reference("bla"));
+    request.getAttributes().put("SRID", "asdf1234");
+    request.setDate(new Date());
+    request.setHostRef("bla");
+    resource.init(null, request, new Response(null));
+
+    resource.handle();
+    assertEquals(Status.SUCCESS_OK, resource.getResponse().getStatus());
+  }
+
   @Test
   public void testForm2invocation() throws Message {
     Form form =
@@ -235,8 +255,9 @@ public class TestScriptingResource {
 
   @Test
   public void testHandleForm() throws Message, IOException {
-    Form form =
-        new Form("-Ooption=OPTION&call=CALL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED");
-    assertEquals(-1, resource.handleForm(form));
+    Subject user = SecurityUtils.getSubject();
+    user.login(AnonymousAuthenticationToken.getInstance());
+    Form form = new Form("call=anonymous_ok");
+    assertEquals(0, resource.handleForm(form));
   }
 }
diff --git a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
index 4ff5d7185d43704e4bc97ef435a5c853e58e3eb8..075c8d998acdfc7f897f9429de468034a4dc022e 100644
--- a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
+++ b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
@@ -5,6 +5,7 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
 import java.io.IOException;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -26,7 +27,13 @@ public class WebinterfaceUtilsTest {
     WebinterfaceUtils utils = new WebinterfaceUtils(new Reference("https://host:2345/some_path"));
     String buildNumber = utils.getBuildNumber();
     String ref = utils.getWebinterfaceURI("sub");
-    assertEquals("https://host:2345/webinterface/" + buildNumber + "/sub", ref);
+    String contextRoot = CaosDBServer.getServerProperty(ServerProperties.KEY_CONTEXT_ROOT);
+    contextRoot =
+        contextRoot != null && contextRoot.length() > 0
+            ? "/" + contextRoot.replaceFirst("^/", "").replaceFirst("/$", "")
+            : "";
+
+    assertEquals("https://host:2345" + contextRoot + "/webinterface/" + buildNumber + "/sub", ref);
   }
 
   @Test