diff --git a/Documentation-develop/PARSER.md b/Documentation-develop/PARSER.md
new file mode 100644
index 0000000000000000000000000000000000000000..6303f165397742e861471067ebdbbb9c2f2cd03b
--- /dev/null
+++ b/Documentation-develop/PARSER.md
@@ -0,0 +1,119 @@
+# Parsers and Tokenizer
+
+## Motivation and use cases
+
+Parsing values for special QFQ columns starting from simple lists
+of key-value pairs to enhanced JSON strings.
+
+## Overview of classes
+
+All classes are defined in the namespace `IMATHUZH\Qfq\Core\Parser`.
+
+### StringTokenizer
+
+This class provides a generator that iterates over a string
+and returns tokens bound by predefined delimiters. The delimiters
+are search in a smart way:
+* delimiters escaped with a backslash are ignored in the search
+* the parser can distinguish between escaping and escaped backslashes,
+  i.e. the colon (as a delimiter) is ignored in the string `ab\:cd`
+  but not in `ab\\:cd`
+* a part of a string between quotes is treated as a plain text - all delimiters
+  are ignored (and the quote characters are removed).
+
+#### Examples with delimiters `:,|`:
+
+| Input string      | Resulting sequence of tokens |
+|-------------------|------------------------------|
+| `ab:cd,ef\|gh`    | `'ab' 'cd' 'ef' 'gh'`        |
+| `"ab:cd",ef\\|gh` | `'ab:cd' 'ef\|gh'`           |
+
+#### Usage
+<pre><code class="php">$tokenizer = new StringTokenizer(':,|');
+foreach ($tokenizer->tokenized('ab:cd,ef\|gh') as list($token, $delimiter)) {
+   // $token is an instance of Token class:
+   //     $token->value    is a string representation of the token
+   //     $token->isString is true if the token is a string (quotes were used)
+   //     $token->empty()  is true for a token generated only from whitespace characters
+   // $delimiter === null    when the end of the string is reached
+}
+</code></pre>
+
+### SimpleParser
+
+This class parses a string into a list of tokens separated by delimiters.
+Comparing to `StringTokenizer`, the returned tokens literal values or special objects
+the processing can be tweaked by options provided as an array in the second parameter.
+
+| Parameters key         | Type | Meaning                                                                |
+|------------------------|------|------------------------------------------------------------------------|
+| `OPTION_PARSE_NUMBERS` | bool | Convert tokens to numbers when possible                                |
+| `OPTION_KEEP_SIGN`     | bool | Creates an instance of `SignedNumber` if a number has an explicit sign |
+| `OPTION_KEY_IS_VALUE`  | bool | Keys with no values are assigned its name as the value                 |
+| `OPTION_EMPTY`         | any  | The value used for empty tokens                                        |
+
+Note that the option `OPTION_KEY_IS_VALUE` is not used by `SimpleParser` but it is used
+by derived classes.
+
+**Note**: the option `OPTION_KEEP_SIGN` is used by `jwt` column, so that claims
+`exp` and `nbf` can be specified either with absolute (no plus) or relative
+(with a plus) timestamps.
+
+#### Usage
+
+<pre><code class="php">$parser = new SimpleParser(":|");
+// By default five special values are configured:
+//    'null' -> null
+//    'true', 'yes' -> true
+//    'false', 'no' -> false
+// More can be defined by updating $specialValues property:
+$parser->specialValues['qfq'] = 'QFQ is great';
+
+// This returns an array ['abc', 'efg', 123, true, 'QFQ is great']
+$parser->parse("abc:efg|123|yes:qfq");
+
+// The tokens can be iterated as follows
+foreach($parser->iterate("abc:efg|123|yes") as $token) {
+   ...
+}
+</code></pre>
+
+### KVPairListParser
+
+This class parses a list of key-value pairs into an associative array.
+It requires two arguments: the list separator and key-value separator.
+
+#### Usage
+<pre><code class="php">// Default separators are , and :
+$parser = new KVPairListParser("|", "=");
+$parser->parse("a=43|b=false|xyz='a|b'");
+// result: [ 'a' => 43, 'b' => false, 'xyz' => 'a|b' ]
+
+foreach ($parser->iterate("a=43|b=false|xyz='a|b'") as $key => $value) {
+   ...
+}
+</code></pre>
+
+### MixedTypeParser
+
+This parser understands both lists and dictionaries and both structures can be nested.
+The constructor must be provided six delimiters in one string: list separator,
+key-value separator, list delimiters (begin and end), and dictionary delimiters
+(begin and end). The default value is `,:[]{}`. It is also possible to replace
+the list and dictionary delimiters with spaces, in which case the parser will
+ignore it. For instance
+* `new MixedTypeParser(',:[]')` can parse nested lists, but not dictionaries (the string is padded)
+* `new MixedTypeParser(',:  {}')` can parse nested dictionaries, but not lists
+
+This parser can be seen as an extension to a JSON parser: strings does not have
+to be enclosed with quotes.
+
+#### Usage
+
+<pre><code class="php">$parser = new MixedTypeParser(',:[]{}', [ /* options */ ]);
+$parser->parse('[0, { a: 14, b: 16 }, abc]');
+$parser->parseList('abc, [x, y, z], {a:15}, xyz');
+$parser->parseDictionary('num:15, arr:[x, y, z], dict:{a:15}, str:xyz');
+</code></pre>
+
+**Note**: there is no meaningful `iterate()` method.
diff --git a/Documentation/Report.rst b/Documentation/Report.rst
index 9ac07ad5b681bc94767079f34ef71e6206af5903..74ce355f6fb7c0863fe858f51f34db6ac3c785b7 100644
--- a/Documentation/Report.rst
+++ b/Documentation/Report.rst
@@ -779,6 +779,8 @@ Summary:
 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 |_decrypt                |:ref:`column-decrypt` - Decrypt value.                                                                                                                                                       |
 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|_jwt                    |:ref:`column-jwt` - generates a json web token from the provided data.                                                                                                                       |
++------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 
 
 .. _column-link:
@@ -1869,6 +1871,38 @@ Decrypting selected columns or strings which are encrypted with QFQ.
 
     10.sql = SELECT secret AS _decrypt FROM Person WHERE id = 1
 
+.. _column-jwt:
+
+Column: _jwt
+^^^^^^^^^^^^
+
+Creates a `json web token <https://jwt.io/>`_ from the provided data.
+
+Supported options:
+
++-----------+---------------+------------------------------------------------------+
+| Parameter | Default value | Note                                                 |
++===========+===============+======================================================+
+| `alg`     | `HS256`       | The signing algorithm - it is included in the header |
+| `key`     | (none)        | The secret key used for signing the token            |
++-----------+---------------+------------------------------------------------------+
+
+Predefined claims:
+
++-------+----------------+-------------------+-----------------------------------------------------------+
+| Claim | Present        | Default value     | Note                                                      |
++=======+================+===================+===========================================================+
+| `iss` | always         | `qfq`             | The default value might be also specified in QFQ settings |
+| `iat` | always         | current timestamp | Ignores any provided value                                |
+| `exp` | when specified | none              | Prefix with `+` to specify a relative timestamp           |
+| `nbf` | when specified | none              | Prefix with `+` to specify a relative timestamp           |
++-------+----------------+-------------------+-----------------------------------------------------------+
+
+**Syntax** ::
+
+    10.sql = SELECT 'exp:+3600,data:{msg:default alg}|<secretKey>' AS _jwt
+    20.sql = SELECT 'exp:+60,data:{msg:explicit agl}|<secretKey>|ES384' AS _jwt
+
 .. _copyToClipboard:
 
 Copy to clipboard
diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index 3e93c4e96b0b10206d46428fad0254eb524be50f..baf66e0fa044c08463acc1e49b37ca6f43a8f1fa 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -1855,6 +1855,8 @@ const COLUMN_STORE_USER = '=';
 
 const COLUMN_FORM_JSON = 'formJson';
 
+const COLUMN_JWT = 'jwt';
+
 // Author: Enis Nuredini
 const COLUMN_ENCRYPT = 'encrypt';
 const COLUMN_DECRYPT = 'decrypt';
diff --git a/extension/Classes/Core/Helper/OnString.php b/extension/Classes/Core/Helper/OnString.php
index 4a22778881710a77760cad737a20bc87086a4bdb..87c099be40a5747bcaa64576a117903e7b74ba97 100644
--- a/extension/Classes/Core/Helper/OnString.php
+++ b/extension/Classes/Core/Helper/OnString.php
@@ -811,7 +811,7 @@ class OnString {
             return array();
         }
 
-        // Search biggest element.
+        // Search the biggest element.
         foreach ($arr as $key => $value) {
 
             if (is_array($value)) {
@@ -858,7 +858,7 @@ class OnString {
                         ERROR_MESSAGE_TO_DEVELOPER => "max: $max, current: " . $currentLength . ", new reduced: " . $newLen]), ERROR_MISSING_OPEN_DELIMITER);
                 } else {
                     // Dive deeper to replace the next biggest element.
-                    $arrNew = $self::limitSizeJsonEncode($arrNew, $newLen, $maxValue);
+                    $arrNew = self::limitSizeJsonEncode($arrNew, $newLen, $maxValue);
                 }
             }
         }
diff --git a/extension/Classes/Core/Helper/Support.php b/extension/Classes/Core/Helper/Support.php
index 3495cd3b8634bcd6ff3baa934aeab8b5711550e8..4dbcbc2f731b6f795ba2eef7f436bb0b12ff699d 100644
--- a/extension/Classes/Core/Helper/Support.php
+++ b/extension/Classes/Core/Helper/Support.php
@@ -152,7 +152,7 @@ class Support {
     public static function arrayToQueryString(array $queryArray) {
         $items = array();
 
-        // CR/Workaround for broken behaviour: LINK() class expects 'id' always as first paramter
+        // CR/Workaround for broken behaviour: LINK() class expects 'id' always as first parameter
         // Take care that Parameter 'id' is the first one in the array:
         if (isset($queryArray[CLIENT_PAGE_ID])) {
             $id = $queryArray[CLIENT_PAGE_ID];
@@ -290,7 +290,7 @@ class Support {
         switch (strtolower($type)) {
             case 'size':
             case 'maxlength':
-                // empty or '0' for attributes of type 'size' or 'maxlength' result in unsuable input elements: skip this.
+                // empty or '0' for attributes of type 'size' or 'maxlength' result in unusable input elements: skip this.
                 if ($value === '' || $value == 0) {
                     return '';
                 }
@@ -378,7 +378,7 @@ class Support {
     }
 
     /**
-     * Search for the parameter $needle in $haystack. The arguments has to be separated by ','.
+     * Search for the parameter $needle in $haystack. The arguments have to be separated by ','.
      *
      * Returns false if not found, or index (starting with 0) of found place. Be careful: use unary operator to compare for 'false'
      *
@@ -630,7 +630,7 @@ class Support {
     }
 
     /**
-     * Returns a representation of 0 in a choosen variant.
+     * Returns a representation of 0 in a chosen variant.
      *
      * @param string $dateFormat FORMAT_DATE_INTERNATIONAL | FORMAT_DATE_GERMAN
      * @param string $showZero
@@ -718,7 +718,7 @@ class Support {
                 $placeholder = $timePattern;
                 break;
             default:
-                throw new \UserFormException("Unexpected Formelement type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE);
+                throw new \UserFormException("Unexpected form element type: '" . $formElement[FE_TYPE] . "'", ERROR_FORMELEMENT_TYPE);
         }
 
         return $placeholder;
@@ -726,7 +726,7 @@ class Support {
 
 
     /**
-     * Encrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces.
+     * Encrypt curly braces by an uncommon string. Helps to prevent unwanted actions on curly braces.
      *
      * @param string $text
      *
@@ -740,7 +740,7 @@ class Support {
     }
 
     /**
-     * Decrypt curly braces by an uncommon string. Helps preventing unwished action on curly braces
+     * Decrypt curly braces by an uncommon string. Helps to prevent unwanted actions on curly braces
      *
      * @param string $text
      *
@@ -760,7 +760,7 @@ class Support {
 
     /**
      * Creates a random string, starting with uniq microseconds timestamp.
-     * After a discussion of ME & CR, the uniqid() should be sufficient to guarantee uniqness.
+     * After a discussion of ME & CR, the uniqid() should be sufficient to guarantee uniqueness.
      *
      * @param int $length Length of the required hash string
      *
@@ -782,7 +782,7 @@ class Support {
     }
 
     /**
-     * Concatenate URL and Parameter. Depending of if there is a '?' in URL or not,  append the param with '?' or '&'..
+     * Concatenate URL and Parameter. Depending on if there is a '?' in URL or not,  append the param with '?' or '&'..
      *
      * @param string $url
      * @param string|array $param
@@ -1316,7 +1316,7 @@ class Support {
 
     /**
      * Check $arr, if there is an element $index. If not, set it to $value.
-     * If  $overwriteThis!=false, replace the the original value with $value, if $arr[$index]==$overwriteThis.
+     * If  $overwriteThis!=false, replace the original value with $value, if $arr[$index]==$overwriteThis.
      *
      * @param array $arr
      * @param string $index
diff --git a/extension/Classes/Core/Parser/KVPairListParser.php b/extension/Classes/Core/Parser/KVPairListParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca04529d6b99f4d0842ff6761a1d7de307931712
--- /dev/null
+++ b/extension/Classes/Core/Parser/KVPairListParser.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Class KVPairListParser
+ *
+ * A parser for lists of key-value pairs with simple values.
+ *
+ * @package qfq
+ */
+class KVPairListParser extends SimpleParser {
+
+    /** @var string the separator for different pairs */
+    private string $listsep;
+
+    /** @var string the character separating a key from its value */
+    private string $kvsep;
+
+
+    public function __construct(string $listsep, string $kvsep, array $options = []) {
+        parent::__construct("$listsep$kvsep", $options);
+        $this->listsep = $listsep;
+        $this->kvsep = $kvsep;
+    }
+
+    /**
+     * Iterates over the string and returns keys with values. Keys are always
+     * treated as strings and only values are processed and, when necessary,
+     * converted to numbers or special values.
+     *
+     * Usage:
+     *
+     *   foreach($parser->iterate($input) as $key => $value) {
+     *       ...
+     *   }
+     *
+     * Examples
+     *   pair separator:       |
+     *   key-value separator:  =
+     *
+     *     a=43|b=15         -->   'a'=>43    'b'=>15
+     *     a='x|y' | 13=87   -->   'a'=>'x|y' '13'=>87
+     *
+     * @param string $data
+     * @return \Generator
+     */
+    public function iterate(string $data): \Generator {
+        // Iterate over token provided by the base tokenizer class
+        $tokens = $this->tokenized($data);
+        while ($tokens->valid()) {
+            // Get the key first
+            list($keyToken, $delimiter) = $tokens->current();
+            $key = strval($keyToken);
+            $tokens->next();
+            if ($delimiter == $this->kvsep) {
+                // The key-value separator is found - find the corresponding value
+                if ($tokens->valid()) {
+                    list($valueToken, $delimiter) = $tokens->current();
+                    $tokens->next();
+                    $empty = $valueToken->empty();
+                    $value = $this->process($valueToken);
+                } else {
+                    // The end of the string - the value is empty
+                    $delimiter = null;
+                    $empty = true;
+                }
+                // Replace an empty token with the empty value
+                yield $key => $empty ? (
+                    $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE]
+                ) : $value;
+            } elseif ($key) {
+                // When no key-value separator, then do nothing it the key is empty
+                // In other words: ignore trailing commas
+                yield $key => $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE];
+            }
+            // Check if the current delimiter is a correct one
+            if ($delimiter && $delimiter != $this->listsep) {
+                $this->raiseUnexpectedDelimiter(
+                    $delimiter,
+                    $this->offset(),
+                    $this->listsep
+                );
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/extension/Classes/Core/Parser/MixedTypeParser.php b/extension/Classes/Core/Parser/MixedTypeParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..fa72870883ae16a9c297f6e85720b1ab83edc131
--- /dev/null
+++ b/extension/Classes/Core/Parser/MixedTypeParser.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+
+/**
+ * Class MixedTypeParser
+ *
+ * A parser for lists and dictionaries that can be nested.
+ * Requires a string of six delimiters provided in this order:
+ * - separator for list items
+ * - key-value separator
+ * - two delimiters (begin and end) for lists
+ * - two delimiters (begin and end) for dictionaries
+ * The parser can be restricted to only nested lists or only
+ * nested dictionaries by providing a space instead of the
+ * corresponding delimiters.
+ *
+ * @package qfq
+ */
+class MixedTypeParser extends SimpleParser {
+
+    // Internally used constants
+    const SEP = 0;
+    const KVSEP = 1;
+    const LIST_START = 2;
+    const LIST_END = 3;
+    const DICT_START = 4;
+    const DICT_END = 5;
+
+    /** @var string delimiters used by this parser */
+    private string $delimiters = ',:[]{}';
+
+
+    public function __construct(?string $delimiters = null, array $options = []) {
+        if ($delimiters) {
+            $this->delimiters = str_pad($delimiters, 6);
+            $delimiters = str_replace(' ', '', $delimiters);
+        } else {
+            $delimiters = $this->delimiters;
+        }
+        parent::__construct($delimiters, $options);
+    }
+
+    /**
+     * Parses the provided string into a literal value, a non-associative array,
+     * or an associative array. The structures can be nested.
+     *
+     * @param string $data
+     * @return mixed
+     */
+    public function parse(string $data) {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            list($data, $empty, $delimiter) = $this->parseImpl($tokens);
+            if ($delimiter) $this->raiseUnexpectedDelimiter($delimiter, $this->offset());
+            return $data;
+        } else {
+            return $this->options[self::OPTION_EMPTY_VALUE];
+        }
+    }
+
+    /**
+     * Assumes the provided string is a list of items and parses
+     * it into a non-associative array (possibly empty or with only
+     * one element).
+     *
+     * @param string $data
+     * @return array
+     */
+    public function parseList(string $data): array {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            return $this->parseListImpl($tokens, null)[0];
+        } else {
+            return [];
+        }
+    }
+
+    /**
+     * Assumes that the provided string is a dictionary (i.e. a list
+     * of key-value pairs) and parses it into an associative array.
+     *
+     * @param string $data
+     * @return array
+     */
+    public function parseDictionary(string $data): array {
+        $tokens = $this->tokenized($data);
+        if ($tokens->valid()) {
+            return $this->parseDictionaryImpl($tokens, null)[0];
+        } else {
+            return [];
+        }
+    }
+
+
+    /**
+     * The main method of the parser. It looks on the first token
+     * and decides on the following action based on the delimiter
+     * and the value of the token.
+     *
+     * @param \Generator $tokens
+     * @return array
+     */
+    protected function parseImpl(\Generator $tokens): array {
+        // Get a token and the bounding delimiter
+        $tokenData = $tokens->current();
+        $tokens->next();
+        list($token, $delimiter) = $tokenData;
+        $empty = $token->empty();
+        switch ($delimiter) {
+            case $this->delimiters[self::DICT_START]:
+                // The opening delimiter of a dictionary cannot be preceded by a nonempty token
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                // Start parsing the string as a dictionary
+                return $this->parseDictionaryImpl($tokens, $this->delimiters[self::DICT_END]);
+            case $this->delimiters[self::LIST_START]:
+                // The opening delimiter of a list cannot be preceded by a nonempty token
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                // Start parsing the string as a list
+                return $this->parseListImpl($tokens, $this->delimiters[self::LIST_END]);
+            default:
+                // Otherwise process the obtained token
+                return [$this->process($token), $empty, $delimiter];
+        }
+    }
+
+    /**
+     * A helper function that checks if a list of a dictionary is followed
+     * directly by another delimiter (or the end of the string)
+     *
+     * @param \Generator $tokens
+     * @param string|null $current
+     * @return string|null            the next delimiter or null if none
+     */
+    private function checkNextDelimiter(\Generator $tokens, ?string $current): ?string {
+        if ($current && $tokens->valid()) {
+            list($token, $next) = $tokens->current();
+            $token->empty() or $this->raiseUnexpectedToken($this->offset(), $current, $next);
+            $tokens->next();
+            return $next;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Processes the tokens into a list
+     *
+     * @param \Generator $tokens
+     * @param string|null $endDelimiter
+     * @return array
+     */
+    protected function parseListImpl(\Generator $tokens, ?string $endDelimiter): array {
+        $result = [];
+        do {
+            // Get a value to add to the list
+            list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+            switch ($delimiter) {
+                case $this->delimiters[self::SEP]:
+                    $result[] = $value;
+                    break;
+                case $endDelimiter:
+                    // Add an empty element only if there was a comma
+                    if (!$empty || $result) {
+                        $result[] = $value;
+                    }
+                    // The end of the list - check if not followed by a non-empty token
+                    $delimiter = $this->checkNextDelimiter($tokens, $delimiter);
+                    return [$result, false, $delimiter];
+                default:
+                    // Only list item separators are allowed here
+                    $this->raiseUnexpectedDelimiter(
+                        $delimiter,
+                        $this->offset(),
+                        $this->delimiters[self::SEP] . $endDelimiter
+                    );
+            }
+        } while ($tokens->valid());
+        // The string ended with a comma - append an empty string unless
+        // the list is expected to end with a delimiter
+        if ($endDelimiter) $this->raiseUnexpectedEnd($endDelimiter);
+        $result[] = $this->options['empty'];
+        return [$result, false, null];
+    }
+
+    /**
+     * Processes the tokens into a dictionary
+     *
+     * @param \Generator $tokens
+     * @param string|null $endDelimiter
+     * @return array
+     */
+    protected function parseDictionaryImpl(\Generator $tokens, ?string $endDelimiter): array {
+        $result = [];
+        do {
+            // Get the key
+            list($key, $delimiter) = $tokens->current();
+            $key = strval($key);
+            $tokens->next();
+            if ($delimiter == $this->delimiters[self::KVSEP]) {
+                // The key-value separator is found - find the corresponding value
+                if ($tokens->valid()) {
+                    list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+                } else {
+                    // The end of the string - the value is empty
+                    $delimiter = null;
+                    $empty = true;
+                }
+                // Replace an empty token with the empty value
+                $result[$key] = $empty ? (
+                $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE]
+                ) : $value;
+            } elseif ($key) {
+                // When no key-value separator, then do nothing it the key is empty
+                // In other words: ignore trailing commas
+                $result[$key] = $this->options[self::OPTION_KEY_IS_VALUE] ? $key : $this->options[self::OPTION_EMPTY_VALUE];
+            }
+            // Check if the current delimiter is a correct one
+            if ($delimiter == $endDelimiter) {
+                $delimiter = $this->checkNextDelimiter($tokens, $delimiter);
+                return [$result, false, $delimiter];
+            } elseif ($delimiter != $this->delimiters[self::SEP]) {
+                $this->raiseUnexpectedDelimiter(
+                    $delimiter,
+                    $this->offset(),
+                    $this->delimiters[self::SEP] . $endDelimiter
+                );
+            }
+        } while ($tokens->valid());
+        // Trailing commas are ok for objects
+        return [$result, false, null];
+    }
+}
diff --git a/extension/Classes/Core/Parser/SimpleParser.php b/extension/Classes/Core/Parser/SimpleParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..4981cd5f9fa0b4bfcfa31fb1460adff5dd6577bb
--- /dev/null
+++ b/extension/Classes/Core/Parser/SimpleParser.php
@@ -0,0 +1,168 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Represents a number prefixed with a sign. This class is used
+ * to treat such values differently from absolute numbers.
+ * This class implements `JsonSerializable`, so that it is
+ * treated nicely by `json_encode()`.
+ * @package qfq
+ */
+class SignedNumber implements \JsonSerializable {
+
+    /** @var int|float the value of the number */
+    public $value;
+
+    public function __construct($value) {
+        $this->value = $value;
+    }
+
+    public function __toString(): string {
+        return $this->value > 0 ? "+$this->value" : "$this->value";
+    }
+
+    public function jsonSerialize() {
+        return $this->value;
+    }
+}
+
+/**
+ * Class SimpleParser
+ *
+ * A basic parser that splits the provided string at unescaped
+ * and unquoted delimiters. Token processing recognizes numbers
+ * and specials values.
+ *
+ * @package qfq
+ */
+class SimpleParser extends StringTokenizer {
+
+    /** @var string Option key: replace numeric strings with numbers */
+    const OPTION_PARSE_NUMBERS = 'parse-numbers';
+
+    /** @var string Option key: convert +num and -num to an instance of SignedNumber */
+    const OPTION_KEEP_SIGN = 'keep-sign';
+
+    /** @var string Option key: empty keys will be assigned their names are values */
+    const OPTION_KEY_IS_VALUE = 'key-is-value';
+
+    /** @var string Option key: the value assigned to empty tokens */
+    const OPTION_EMPTY_VALUE = 'empty';
+
+    /** @var array a configuration of the parser */
+    public array $options = [
+        self::OPTION_KEEP_SIGN => false,     // if true, tokens "+number" and "-number" are converted to instances of SignedNumber
+        self::OPTION_KEY_IS_VALUE => false,  // if true, a key with no value is assigned its name
+        self::OPTION_EMPTY_VALUE => null,    // the value used for empty tokens
+        self::OPTION_PARSE_NUMBERS => true   // if true, tokens are replaced with numbers if possible
+    ];
+
+    /**
+     * @var array A dictionary for special values of tokens. These values are
+     *            used only for tokens for which the property `isString` is false.
+     */
+    public array $specialValues = [
+        'null' => null,
+        'true' => true,
+        'false' => false,
+        'yes' => true,
+        'no' => false
+    ];
+
+
+    public function __construct(string $delimiters, array $options = []) {
+        parent::__construct($delimiters);
+        $this->options = array_merge($this->options, $options);
+    }
+
+    /**
+     * Processes a token into a string, a number, or a special value.
+     * @return mixed
+     */
+    protected function process(Token $token) {
+        $asString = strval($token);
+        if ($token->isString) {
+            return $asString;
+        } elseif ($asString === '') {
+            return $this->options[self::OPTION_EMPTY_VALUE];
+        } elseif ($this->options[self::OPTION_PARSE_NUMBERS] && is_numeric($asString)) {
+            if (preg_match("/^[+-]?\d+$/", $asString)) {
+                $value = intval($asString);
+            } else {
+                $value = floatval($asString);
+            }
+            return ($this->options[self::OPTION_KEEP_SIGN] && ($asString[0] === '+' || $asString[0] === '-'))
+                ? new SignedNumber($value) : $value;
+        } elseif (array_key_exists($asString, $this->specialValues)) {
+            // isset() does not work, because the array has `null` as one of values
+            return $this->specialValues[$asString];
+        } else {
+            return $asString;
+        }
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $delimiter
+     * @param $position
+     * @param $expected
+     */
+    protected static function raiseUnexpectedDelimiter($delimiter, $position, $expected=null): void {
+        $msg = "An unexpected '$delimiter' at position $position";
+        if ($expected) {
+            $msg .= " while expecting " . implode(' or ', str_split($expected));
+        }
+        throw new \RuntimeException($msg);
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $expected
+     */
+    protected static function raiseUnexpectedEnd($expected): void {
+        throw new \RuntimeException("An unexpected end while searching for '$expected'");
+    }
+
+    /**
+     * A helper method that throws an exception
+     * @param $position
+     * @param $before
+     * @param $after
+     */
+    protected static function raiseUnexpectedToken($position, $before, $after): void {
+        $msg = "An unexpected token at $position";
+        $extra = [];
+        if ($before) $extra[] = " before '$before'";
+        if ($after) $extra[] = " after '$after'";
+        $msg .= implode('and ', $extra);
+        throw new \RuntimeException($msg);
+    }
+
+    /**
+     * Parses the provided string into a list of values separated by delimiters.
+     *
+     * @param string $data
+     * @return mixed
+     */
+    public function parse(string $data) {
+        return iterator_to_array($this->iterate($data));
+    }
+
+    /**
+     * Iterates over pieces of a string separated by unescaped delimiters.
+     *
+     * @param string $data
+     * @return \Generator
+     */
+    public function iterate(string $data): \Generator {
+        foreach ($this->tokenized($data) as $token) {
+            yield $this->process($token[0]);
+        }
+    }
+}
diff --git a/extension/Classes/Core/Parser/StringTokenizer.php b/extension/Classes/Core/Parser/StringTokenizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..be3bf3093eb68f6c9165890835f158a7d7911b2f
--- /dev/null
+++ b/extension/Classes/Core/Parser/StringTokenizer.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Class StringTokenizer
+ *
+ * This class is used to parse a string into a sequence of tokens
+ * bound by provided delimiters. While parsing the string, pieces
+ * of a token are generated each time a delimiter, a quote or
+ * a backslash is found. They are joined together and yielded
+ * once an unescaped delimiter is encountered or the end of
+ * the string is reached.
+ *
+ * @project qfq
+ */
+class StringTokenizer {
+
+    /** @var string a regexp pattern to match delimiters */
+    private string $delimitersPattern;
+
+    /** @var int the offset for the current token piece */
+    private int $currentOffset = 0;
+
+    /** @var TokenBuilder the object for building a token */
+    protected TokenBuilder $tokenBuilder;
+
+
+    public function __construct(string $delimiters) {
+        $escapedDelimiters = str_replace(
+            ['[', ']', '/', '.'],
+            ['\\[', '\\]', '\\/', '\\.'],
+            $delimiters
+        );
+        $this->delimitersPattern = "/[$escapedDelimiters\\\\\"']/";
+        $this->tokenBuilder = new TokenBuilder();
+    }
+
+    /**
+     * The offset of the last found delimiter or -1 otherwise
+     * @return int
+     */
+    public function offset(): int {
+        return $this->currentOffset-1;
+    }
+
+    /**
+     * Iterates over unescaped delimiters and quotes from a provided string.
+     * At each iteration returns a delimiter or null when the end of the
+     * string is reached.
+     *
+     * Examples
+     * delimiters: ,:|
+     *    ab:cd, ef|gh    -->  : , | (null)    (token pieces: 'ab' 'cd' 'ef' 'gh')
+     *    ab\:c d,"e:f"   -->  , " : " (null)  (token pieces: 'ab:c d' '' 'e' 'f' '')
+     *    ab\\:cd,' '     -->  : , (null)      (token pieces: 'ab\' 'cd' ' ')
+     *
+     * @param string $data   the searched string
+     * @return \Generator    delimiters
+     */
+    protected function unescapedDelimitersAndQuotes(string $data): \Generator {
+        // Reset the token builder and offset
+        $this->tokenBuilder->reset();
+        $this->currentOffset = 0;
+        // Match all delimiters, including escaped and inside quotes
+        if (preg_match_all($this->delimitersPattern, $data, $delimiters, PREG_OFFSET_CAPTURE)) {
+            // If non-empty, $delimiters is an array with one element:
+            // a list of pairs [delimiter, offset]
+            $delimiters = $delimiters[0];
+            $tokenData = current($delimiters);
+            while ($tokenData) {
+                list($delimiter, $offset) = $tokenData;
+                if ($delimiter == '\\') {
+                    // The next character is escaped. If it is a delimiter,
+                    // then we will ignore it and continue the search.
+                    $tokenData = next($delimiters);
+                    if (!$tokenData) {
+                        // No more delimiters - we have reached the end of the string.
+                        // We return the remaining part of the string outside the loop.
+                        break;
+                    } elseif ($tokenData[1] == $offset + 1) {
+                        // This delimiter or quote is escaped by the backslash
+                        if ($tokenData[0] != '\\') {
+                            $this->tokenBuilder->append(substr($data, $this->currentOffset, $offset-$this->currentOffset));
+                            $this->currentOffset = $tokenData[1];
+                        }
+                        $tokenData = next($delimiters);
+                        continue;
+                    }
+                }
+                // An unescaped delimiter has been found
+                $this->tokenBuilder->append(substr($data, $this->currentOffset, $offset-$this->currentOffset));
+                $this->currentOffset = $offset + 1;
+                yield $delimiter;
+                $tokenData = next($delimiters);
+            }
+        }
+    }
+
+    /**
+     * Iterates over unescaped and unquoted delimiters from a provided string.
+     * Unescaped quotes must match and are not included in the resulting token.
+     * At each iteration returns a pair consisting of
+     * - the token generated from the substring bound by the previous
+     *   and the current delimiter
+     * - the delimiter
+     * Note that the offset of the current delimiter is given by the offset()
+     * method. The delimiter is null when the end of the string is reached.
+     *
+     * Examples
+     * delimiters: ,:|
+     *    ab:cd, ef|gh    -->  ['ab', ':'] ['cd', ','] ['ef', '|'] ['gh', (null)]
+     *    ab\:c d,"e:f"   -->  ['ab:c d', ','] ['e f', (null)]
+     *    ab\\:cd,' '     -->  ['ab\', ':'] ['cd', ','] [' ', (null)]
+     *
+     * @param string $data   the string to search for delimiters
+     * @return \Generator    pairs [token, delimiter]
+     */
+    public function tokenized(string $data): \Generator {
+        // Iterate over all unescaped delimiters
+        $delimitersAndQuotes = $this->unescapedDelimitersAndQuotes($data);
+        while ($delimitersAndQuotes->valid()) {
+            $delimiter = $delimitersAndQuotes->current();
+            if ($delimiter === '"' || $delimiter === "'") {
+                // We will search for the matching quote and put everything
+                // in between to the token
+                $quote = $delimiter;
+                $this->tokenBuilder->markQuote();
+                while (true) {
+                    // Get next delimiter and check if it is a matching quote
+                    $delimitersAndQuotes->next();
+                    if (!$delimitersAndQuotes->valid()) {
+                        throw new \RuntimeException("An unexpected end while searching for '$delimiter'");
+                    }
+                    $delimiter = $delimitersAndQuotes->current();
+                    if ($delimiter === $quote) {
+                        // We have found a quote - break this loop and continue
+                        // searching for delimiters
+                        $this->tokenBuilder->markQuote();
+                        break;
+                    }
+                    // An quoted delimiter is a part of the token
+                    $this->tokenBuilder->pieces[] = $delimiter;
+                }
+            } else {
+                // An unescaped delimiter: return the current token
+                // and start building the next one
+                yield [$this->tokenBuilder->process(), $delimiter];
+            }
+            $delimitersAndQuotes->next();
+        }
+        // No more delimiters: add the rest of the string and process the token
+        $this->tokenBuilder->pieces[] = substr($data, $this->currentOffset);
+        $token = $this->tokenBuilder->process();
+        $token->empty() or yield [$token, null];
+    }
+}
diff --git a/extension/Classes/Core/Parser/Token.php b/extension/Classes/Core/Parser/Token.php
new file mode 100644
index 0000000000000000000000000000000000000000..fe9c43a28aea03bb77b4272576f17cbbd29c5f99
--- /dev/null
+++ b/extension/Classes/Core/Parser/Token.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A token returned by StringTokenizer when parsing a string.
+ * It represents a part of the input string except that unescaped quotes
+ * and backslashes before delimiters and quotes are removed.
+ * @package qfq
+ */
+class Token {
+    /**
+     * @var string The string wrapped by the token.
+     */
+    public string $value;
+
+    /**
+     * @var bool True if the token must be treated as a string
+     *           (for instance it was surrounded with quotes)
+     */
+    public bool $isString;
+
+    public function __construct(string $value, bool $isString) {
+        $this->value = $value;
+        $this->isString = $isString;
+    }
+
+    /**
+     * Returns true when the token is empty. In particular, this means
+     * that there were no quotes in the token.
+     * @return bool
+     */
+    public function empty() : bool {
+        return $this->value === '' && !$this->isString;
+    }
+
+    public function __toString(): string {
+        return $this->value;
+    }
+}
diff --git a/extension/Classes/Core/Parser/TokenBuilder.php b/extension/Classes/Core/Parser/TokenBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..92f714187c9b953b6feb5abf1e8e403237769714
--- /dev/null
+++ b/extension/Classes/Core/Parser/TokenBuilder.php
@@ -0,0 +1,101 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A helper class used by StringTokenizer to build a token. It contains extra information
+ * that are necessary to properly process the token into a string.
+ * @package qfq
+ */
+class TokenBuilder {
+
+    /**
+     * @var array A list of substrings that form the token. Splits occur at escaped
+     * delimiters and unescaped quotes so that they do not appear in a final string.
+     */
+    public array $pieces = [];
+
+    /**
+     * @var int The current total length of the token pieces
+     */
+    public int $length = 0;
+
+    /**
+     * @var int the offset of the first unescaped quote
+     */
+    public int $firstQuoteOffset = -1;
+
+    /**
+     * @var int the offset of the last unescaped quote
+     */
+    public int $lastQuoteOffset = -1;
+
+    /**
+     * Returns true when an unescaped quote has been found
+     * while creating the token.
+     * @return bool
+     */
+    public function hasQuotes(): bool {
+        return $this->firstQuoteOffset >= 0;
+    }
+
+    /**
+     * Resets the builder to its initial state
+     * @return void
+     */
+    public function reset()
+    {
+        $this->pieces = [];
+        $this->length = 0;
+        $this->firstQuoteOffset = -1;
+        $this->lastQuoteOffset = -1;
+    }
+
+    /**
+     * Processes the data to a token and resets the builder
+     * @return Token
+     */
+    public function process(): Token {
+        // Combine all the pieces and trim the resulting string,
+        // but keep whitespaces that were surrounded by quotes
+        $value = implode('', $this->pieces);
+        if ($this->hasQuotes()) {
+            $value = ltrim(substr($value, 0, $this->firstQuoteOffset)) .
+                     substr($value, $this->firstQuoteOffset, $this->lastQuoteOffset-$this->firstQuoteOffset) .
+                     rtrim(substr($value, $this->lastQuoteOffset));
+        } else {
+            $value = trim($value);
+        }
+        $token = new Token($value, $this->hasQuotes());
+        $this->reset();
+        return $token;
+    }
+
+    /**
+     * Adds a piece of a token and updates the builder status
+     * @param string $data
+     * @return void
+     */
+    public function append(string $data) {
+        $this->pieces[] = $data;
+        $this->length += strlen($data);
+    }
+
+    /**
+     * Notifies the builder that a quote has been encountered.
+     * The builder updates offsets of quotes accoringly.
+     * @return void
+     */
+    public function markQuote() {
+        if ($this->firstQuoteOffset < 0) {
+            $this->firstQuoteOffset = $this->length;
+        } else {
+            $this->lastQuoteOffset = $this->length;
+        }
+    }
+}
diff --git a/extension/Classes/Core/Report/Report.php b/extension/Classes/Core/Report/Report.php
index 332b4c614c99065e6ccb0c6159252c80eef67cca..ce3b53ea6aa9d562efaae067468b4751d2cad035 100644
--- a/extension/Classes/Core/Report/Report.php
+++ b/extension/Classes/Core/Report/Report.php
@@ -34,10 +34,15 @@ use IMATHUZH\Qfq\Core\Helper\OnString;
 use IMATHUZH\Qfq\Core\Helper\Path;
 use IMATHUZH\Qfq\Core\Helper\Sanitize;
 use IMATHUZH\Qfq\Core\Helper\Support;
+use IMATHUZH\Qfq\Core\Parser\MixedTypeParser;
+use IMATHUZH\Qfq\Core\Parser\SignedNumber;
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
 use IMATHUZH\Qfq\Core\Store\Sip;
 use IMATHUZH\Qfq\Core\Store\Store;
 use IMATHUZH\Qfq\Core\Typo3\T3Handler;
 
+use Firebase\JWT\JWT;
+
 const DEFAULT_QUESTION = 'question';
 const DEFAULT_ICON = 'icon';
 const DEFAULT_BOOTSTRAP_BUTTON = 'bootstrapButton';
@@ -1366,6 +1371,54 @@ class Report {
                     $content .= Support::encryptDoubleCurlyBraces(FormAsFile::renderColumnFormJson($columnValue, $dbQfq));
                     break;
 
+                // Author: Krzysztof Putyra
+                case COLUMN_JWT:
+                    /* Converts a string
+                     *  claim1:value, claim2:value, ... | key | alg
+                     * into a json web token. Parameters:
+                     * - alg    the name of the signing algorithm (default: HS256)
+                     * - key    the secret key used by the signing algorithm
+                     * Standard claims with an extended interpretation of values:
+                     * - iss    the issuer of the token (default: qfq)
+                     * - iat    the timestamp the token has been issued (default: current)
+                     * - exp    the expiration timestamp or the number of seconds till invalid (if prefixed with '+')
+                     * - nbf    the timestamp from when or (if prefixed with '+') the number of seconds after which the token is valid
+                     */
+
+                    // Split the column into |-separated sections
+                    $parser = new SimpleParser('|', [
+                        SimpleParser::OPTION_EMPTY_VALUE => ''
+                    ]);
+                    $splitContent = $parser->parse($columnValue);
+                    // Check that key is provided
+                    if (count($splitContent) < 2) {
+                        throw new \UserReportException("JWT requires a secret key, but it is missing");
+                    }
+
+                    // Parse the payload
+                    $currentTime = time();
+                    $parser = new MixedTypeParser(null, [
+                        SimpleParser::OPTION_KEEP_SIGN => true
+                    ]);
+                    $claims = array_merge(
+                        ['iss' => 'qfq', 'iat' => $currentTime],
+                        $parser->parseDictionary($splitContent[0])
+                    );
+                    foreach (['exp', 'nbf', 'iat'] as $claim) {
+                        $value = $claims[$claim] ?? 0;
+                        if ($value instanceof SignedNumber) {
+                            $claims[$claim] = $value->value + $currentTime;
+                        }
+                    }
+
+                    // Create the token
+                    $content .= JWT::encode(
+                        $claims,
+                        $splitContent[1],
+                        $splitContent[2] ?? 'HS256'
+                    );
+                    break;
+
                 // Author: Enis Nuredini
                 case COLUMN_ENCRYPT:
                     $encryptionMethodColumn = $this->store->getVar(SYSTEM_ENCRYPTION_METHOD, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
diff --git a/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php b/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ae87a6c8b81293ef5dfc1493010e7e04b8ae069b
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\KVPairListParser;
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
+use PHPUnit\Framework\TestCase;
+
+class KVPairListParserTest extends TestCase {
+    public function testEmptyString() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            [],
+            $parser->parse('')
+        );
+    }
+
+    public function testSinglePair() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(['a' => 42], $parser->parse('a:42'));
+    }
+
+    public function testSingleKey() {
+        $parser = new KVPairListParser('|', ':', [
+            SimpleParser::OPTION_KEY_IS_VALUE => true
+        ]);
+        $this->assertEquals(['a' => 'a'], $parser->parse('a:'));
+
+        $parser = new KVPairListParser('|', ':', [
+            SimpleParser::OPTION_KEY_IS_VALUE => false,
+            SimpleParser::OPTION_EMPTY_VALUE => 18
+        ]);
+        $this->assertEquals(['a' => 18], $parser->parse('a:'));
+        $this->assertEquals(['a' => 18], $parser->parse('a'));
+    }
+
+    public function testSimpleList() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab'=>'x','cd'=>'y','ef'=>'z'],
+            $parser->parse('ab:x|cd:y|ef:z')
+        );
+    }
+
+    public function testEscapedSeparators() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab' => 'x','cd:y|ef' => 'z:a'],
+            $parser->parse('ab:x|cd\\:y\\|ef:z\\:a')
+        );
+    }
+
+    public function testQuotedSeparators() {
+        $parser = new KVPairListParser('|', ':');
+        $this->assertEquals(
+            ['ab' => 'x','cd:y| ef' => 'z:a'],
+            $parser->parse('ab:x|"cd:y| ef":z":a"')
+        );
+    }
+
+    public function testIterate() {
+        $parser = new KVPairListParser('|', ':');
+        $iterator = $parser->iterate('a:1|b:2|c:3');
+        $expected = ['a' => 1, 'b' => 2, 'c' => 3];
+        foreach($iterator as $key => $value) {
+            $expectedKey = key($expected);
+            $expectedValue = current($expected);
+            $this->assertSame($expectedKey, $key);
+            $this->assertSame($expectedValue, $value);
+            next($expected);
+        }
+        $this->assertFalse(current($expected));
+    }
+}
diff --git a/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php b/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..66f9f69b78e6afdf219e41fc62e395cf6f167b5c
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php
@@ -0,0 +1,192 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\MixedTypeParser;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Class MixedTypeParserTest
+ * @package qfq
+ */
+class MixedTypeParserTest extends TestCase {
+
+    protected function assertGeneratorOutput($generator, $output) {
+        foreach ($output as $value) {
+            $data = $generator->current();
+            $data[0] = strval($data[0]);
+            $this->assertSame($value, $data, "Expecting " . json_encode($value));
+            $generator->next();
+        }
+        $this->assertNull($generator->current());
+    }
+
+    public function testLiteralValues() {
+        $parser = new MixedTypeParser(null, ['empty' => 18]);
+        $data = [
+            'abc' => 'abc',   ' x\\:y  ' => 'x:y',
+            ''  => 18,        '""' => '',
+            ' ' => 18,        '" "' => ' ',
+            '123' => 123,     '"123"' => '123',
+            '12.5' => 12.5,   '"12.5"' => '12.5',
+            'false' => false, '"false"' => 'false',
+            'true' => true,   '"true"' => 'true',
+            'yes' => true,    '"yes"' => 'yes',
+            'no' => false,    '"no"' => 'no',
+            'null' => null,   '"null"' => 'null'
+        ];
+        foreach ($data as $input => $expected)
+            $this->assertSame($expected, $parser->parse($input));
+    }
+
+    public function testParseFlatList() {
+        $parser = new MixedTypeParser();
+        // Empty list
+        $this->assertEquals([], $parser->parseList(''));
+        $this->assertEquals([], $parser->parseList('  '));
+        // Compact list
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parseList("a,b,c,d")
+        );
+        // Spaces are stripped
+        $this->assertEquals(
+            ['a', 'b', 'c'],
+            $parser->parseList("a, b,  c ")
+        );
+        // Internal spaces are preserved
+        $this->assertEquals(
+            ['a', 'b c', 'd'],
+            $parser->parseList("a, b c , d")
+        );
+        // Escaped commas are ignored
+        $this->assertEquals(
+            ['a,b', 'c'],
+            $parser->parseList("a\\,b,c")
+        );
+        // Quoted commas are ignored
+        $this->assertEquals(
+            ['a, b', 'c'],
+            $parser->parseList("'a, b', c")
+        );
+        // Trailing comma adds an empty element
+        $this->assertEquals(
+            [null, null, 'c', null],
+            $parser->parseList(",,c,")
+        );
+    }
+
+    public function testParseListValues() {
+        $parser = new MixedTypeParser();
+        $this->assertEquals([], $parser->parse("[]"));
+        $this->assertEquals([], $parser->parse("[ ]"));
+
+      //  $this->assertEquals([''], $parser->parse("['']"));
+
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parse("[a,b,c,d]")
+        );
+        $this->assertEquals(
+            ['a', 'b', 'c', 'd'],
+            $parser->parse(" [a, b, c ,d] ")
+        );
+
+    }
+
+    public function testParseFlatDictionary() {
+        $parser = new MixedTypeParser();
+        // Empty object
+        $this->assertEquals([], $parser->parseDictionary(''));
+        $this->assertEquals([], $parser->parseDictionary('   '));
+        // Compact dictionary
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parseDictionary("a:1,b:2,c:3,d:4")
+        );
+        // Spaces are stripped
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2'],
+            $parser->parseDictionary(" a : 1, b: 2\n\t ")
+        );
+        // Internal spaces are preserved
+        $this->assertEquals(
+            ['a b'=>'1', 'b'=>'2 3 4'],
+            $parser->parseDictionary(" a b: 1, b: 2 3 4")
+        );
+        // Escaped delimiters are ignored
+        $this->assertEquals(
+            ['a,b'=>'1', 'b'=>'2:3,4'],
+            $parser->parseDictionary("a\\,b:1,b:2\\:3\\,4")
+        );
+        // Quoted delimiters are ignored
+        $this->assertGeneratorOutput(
+            $parser->tokenized("'a:1,b':\"23,4\""),
+            [['a:1,b',':'],['23,4',null]]
+        );
+        $this->assertEquals(
+            ['a:1,b' => '23,4'],
+            $parser->parseDictionary("'a:1,b':\"23,4\"")
+        );
+        // Trailing commas are ignored
+        $this->assertEquals(
+            ['a' => 1, 'b' => 2],
+            $parser->parseDictionary('a:1, b:2, ')
+        );
+    }
+
+    public function testParseDictValues() {
+        $parser = new MixedTypeParser();
+        $this->assertEquals([], $parser->parse("{}"));
+        $this->assertEquals([], $parser->parse("{ }"));
+
+        $this->assertEquals(['a' => null], $parser->parse("{a:}"));
+
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parse("{a:1,b:2,c:3,d:4}")
+        );
+
+        $this->assertEquals(
+            ['a'=>'1', 'b'=>'2', 'c'=>'3', 'd'=>'4'],
+            $parser->parse(" { a:1, b:2, c : 3 ,d:4 } ")
+        );
+    }
+
+    public function testParseNestedStructures() {
+        $parser = new MixedTypeParser();
+        // Nested lists
+        $this->assertEquals(
+            ['a', 'b', ['ca', 'cb'], 'd'],
+            $parser->parse("[a, b, [ca, cb], d]")
+        );
+        // Dictionary nested in a list
+        $this->assertEquals(
+            ['a', 'b', ['ca'=>'0', 'cb'=>'1'], 'd'],
+            $parser->parse("[a, b, {ca:0, cb:1}, d]")
+        );
+        // List nested in a dictionary
+        $this->assertEquals(
+            ['a'=>'0', 'b'=>'1', 'c'=>['ca', 'cb'], 'd'=>'3'],
+            $parser->parse("{a:0, b:1, c:[ca, cb], d:3}")
+        );
+        // Nested dictionaries
+        $this->assertEquals(
+            ['a'=>'0', 'b'=>'1', 'c'=>['ca'=>'5', 'cb'=>'6'], 'd'=>'3'],
+            $parser->parse("{a:0, b:1, c:{ca:5, cb:6}, d:3}")
+        );
+    }
+
+    public function testRestrictedParser() {
+        $parser = new MixedTypeParser(',:  {}');
+        $this->assertEquals(
+            ['a' => '[b]', 'c' => '[d', 'e]' => ''],
+            $parser->parse('{a:[b],c:[d,e]}')
+        );
+    }
+}
\ No newline at end of file
diff --git a/extension/Tests/Unit/Core/Parser/SimpleParserTest.php b/extension/Tests/Unit/Core/Parser/SimpleParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6de69da32215b9fe1ce2860f6e97358739928f5
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/SimpleParserTest.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\SimpleParser;
+use PHPUnit\Framework\TestCase;
+
+class SimpleParserTest extends TestCase {
+
+    public function testEmptyString() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            [],
+            $parser->parse('')
+        );
+    }
+
+    public function testSingletons() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(['abcd'], $parser->parse('abcd'));
+        $this->assertEquals([42], $parser->parse('42'));
+    }
+
+    public function testSimpleList() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd','ef'],
+            $parser->parse('ab,cd,ef')
+        );
+        $parser = new SimpleParser('|');
+        $this->assertEquals(
+            ['ab','cd','ef'],
+            $parser->parse('ab|cd|ef')
+        );
+        $parser = new SimpleParser('|,.');
+        $this->assertEquals(
+            ['ab','cd','ef','gh'],
+            $parser->parse('ab|cd,ef.gh')
+        );
+    }
+
+    public function testEscapedSeparators() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd,ef'],
+            $parser->parse('ab,cd\\,ef')
+        );
+    }
+
+    public function testQuotedSeparators() {
+        $parser = new SimpleParser(',');
+        $this->assertEquals(
+            ['ab','cd, ef'],
+            $parser->parse('ab,"cd, ef"')
+        );
+    }
+
+    public function testIterate() {
+        $parser = new SimpleParser(',');
+        $iterator = $parser->iterate('a,b,c');
+        $expected = ['a', 'b', 'c'];
+        foreach($iterator as $value) {
+            $this->assertSame(current($expected), $value);
+            next($expected);
+        }
+        $this->assertFalse(current($expected));
+
+    }
+}
+
diff --git a/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php b/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..cdf586cf755c735473f9f1a9a2b889a70db83653
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @package qfq
+ * @author kputyr
+ * @date: 09.11.2023
+ */
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\StringTokenizer;
+use PHPUnit\Framework\TestCase;
+
+class StringTokenizerTest extends TestCase {
+
+    protected function assertTokenizer($delimiters, $data, $expected) {
+        $parser = new StringTokenizer($delimiters);
+        $generator = $parser->tokenized($data);
+        foreach ($expected as $data) {
+            list($token, $delimiter) = $generator->current();
+            if (is_array($data)) {
+                $this->assertSame($data[0], $token->value, "Expecting $data[0]");
+                $this->assertSame($data[1], $delimiter, "Expecting '$data[1]'");
+            } else {
+                $this->assertSame($data, $token->value, "Expecting $data");
+            }
+            $generator->next();
+        }
+        $this->assertNull($generator->current());
+    }
+
+    public function testOffset() {
+        $parser = new StringTokenizer(':|');
+        $input = 'a:bc:de|f';
+        $offsets = [1,4,7,null];
+        $tokens = $parser->tokenized($input);
+        foreach($offsets as $value) {
+            $tokens->current();
+            if (!is_null($value)) $this->assertSame($value, $parser->offset());
+            $tokens->next();
+        }
+        $this->assertFalse($tokens->valid());
+    }
+
+    public function testEmptyString() {
+        $this->assertTokenizer(':,[]{}', '', []);
+    }
+
+    public function testSimpleString() {
+        $this->assertTokenizer(':,[]{}', 'abc', [['abc',null]]);
+    }
+
+    public function testUnescapedDelimiters() {
+        $this->assertTokenizer(
+            ':,./]{',
+            "x:7,y.z/24]{x",
+            [[ 'x', ':'], ['7', ','], ['y', '.'], ['z', '/'],
+                ['24', ']'], ['','{'], ['x',null]]
+        );
+    }
+
+    public function testEscapedDelimiters() {
+        $this->assertTokenizer(
+            ':,./]{',
+            "x\\:7\\,y\\.z\\/24\\]\\{x",
+            [[ "x:7,y.z/24]{x", null]]
+        );
+    }
+
+    public function testEscapedNonDelimiters() {
+        $this->assertTokenizer(
+            ':',
+            'x\\\\y\\n',
+            ['x\\\\y\\n']
+        );
+    }
+
+    public function testTokensAreTrimmed() {
+        $this->assertTokenizer(
+            '|',
+            ' a|b | cd ',
+            ['a', 'b', 'cd']
+        );
+    }
+
+    public function testSpacesInside() {
+        $this->assertTokenizer(
+            ':',
+            "x y: sy   ca : f\t\ng",
+            ['x y', 'sy   ca', "f\t\ng"]
+        );
+    }
+
+    public function testQuotesAreRemoved() {
+        $this->assertTokenizer(
+            ':',
+            '"x y":ab\'x\'cd',
+            ['x y', 'abxcd']
+        );
+    }
+
+    public function testQuotedSpacesArePreserved() {
+        $this->assertTokenizer(
+            ':',
+            '" "x:ab" x ":\'  \'',
+            [' x', 'ab x ', '  ']
+        );
+    }
+
+    public function testEscapedQuotesArePreserved() {
+        $this->assertTokenizer(
+            ':',
+            '\"x y\":ab\\\'cd',
+            ['"x y"', "ab'cd"]
+        );
+    }
+
+    public function testNestedQuotesAreNotParsed() {
+        $this->assertTokenizer(
+            ':',
+            '"\'":\'"\'',
+            ["'", '"']
+        );
+    }
+
+    public function testQuotedDelimitersAreIgnored() {
+        $this->assertTokenizer(
+            ':,|',
+            'x:a\\|b|c\\,d\\:e:24',
+            ['x', 'a|b', 'c,d:e', '24']
+        );
+    }
+}
diff --git a/extension/composer.json b/extension/composer.json
index 848142983e29bb2e9ddcc9e0b9ab647c57ce8e58..be32013f560e6fa6bf04c5bedcc9ae2fdf879032 100644
--- a/extension/composer.json
+++ b/extension/composer.json
@@ -3,7 +3,8 @@
     "phpoffice/phpspreadsheet": "^1.3",
     "ext-json": "*",
     "twig/twig": "^2.0",
-    "ezyang/htmlpurifier": "^4.15"
+    "ezyang/htmlpurifier": "^4.15",
+    "firebase/php-jwt": "^6.9"
   },
   "require-dev": {
     "phpunit/phpunit": "^9"