diff --git a/extension/Classes/Core/Constants.php b/extension/Classes/Core/Constants.php
index fd6c8745af7d855ef2daaabee9cc6c0a16d6694b..f179bb2b1d54f7cdadaa8607149ea1cf59cdf8d6 100644
--- a/extension/Classes/Core/Constants.php
+++ b/extension/Classes/Core/Constants.php
@@ -1849,6 +1849,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/JWT.php b/extension/Classes/Core/Helper/JWT.php
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/extension/Classes/Core/Parser/KVPairListParser.php b/extension/Classes/Core/Parser/KVPairListParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..279fd69b9dedd543178a09c3536a6170fe99edb0
--- /dev/null
+++ b/extension/Classes/Core/Parser/KVPairListParser.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+
+class KVPairListParser extends SimpleParser
+{
+    private string $listsep;
+    private string $kvsep;
+
+
+    public function __construct(string $listsep, string $kvsep, array $options = [])
+    {
+        parent::__construct("$listsep$kvsep", $options);
+        $this->listsep = $listsep;
+        $this->kvsep = $kvsep;
+    }
+
+    public function iterate(string $data): \Generator
+    {
+        $tokens = $this->tokenized($data);
+        while ($tokens->valid()) {
+            list($keyToken, $delimiter) = $tokens->current();
+            $key = strval($keyToken);
+            $tokens->next();
+            if ($delimiter == $this->kvsep) {
+                if ($tokens->valid()) {
+                    list($valueToken, $delimiter) = $tokens->current();
+                    $tokens->next();
+                    $empty = $valueToken->empty();
+                    $value = $this->process($valueToken);
+                } else {
+                    $delimiter = null;
+                    $empty = true;
+                }
+                yield $key => $empty ? (
+                    $this->options['key-is-value'] ? $key : $this->options['empty']
+                ) : $value;
+            } elseif ($key) {
+                yield $key => $this->options['key-is-value'] ? $key : $this->options['empty'];
+            }
+            if ($delimiter && $delimiter != $this->listsep) {
+                $this->raiseUnexpectedDelimiter(
+                    $delimiter,
+                    $this->offset(),
+                    $this->listsep
+                );
+            }
+        };
+        // Trailing commas are ok for objects
+    }
+}
\ 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..2d9ae35aa3eece4e4a91fbcccae2b7fe670e30ec
--- /dev/null
+++ b/extension/Classes/Core/Parser/MixedTypeParser.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+
+
+class MixedTypeParser extends SimpleParser
+{
+    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['empty'];
+        }
+    }
+
+    /**
+     * 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 [];
+        }
+    }
+
+
+    protected function parseImpl(\Generator $tokens): array
+    {
+        $tokenData = $tokens->current();
+        $tokens->next();
+        list($token, $delimiter) = $tokenData;
+        $empty = $token->empty();
+        switch ($delimiter) {
+            case $this->delimiters[self::DICT_START]:
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                return $this->parseDictionaryImpl($tokens, $this->delimiters[self::DICT_END]);
+            case $this->delimiters[self::LIST_START]:
+                $empty or $this->raiseUnexpectedToken($this->offset(), $delimiter, null);
+                return $this->parseListImpl($tokens, $this->delimiters[self::LIST_END]);
+            case null:
+                return [$this->process($token), $empty, null];
+            default:
+                return [$this->process($token), $empty, $delimiter];
+        }
+    }
+
+    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;
+        }
+    }
+
+    protected function parseListImpl(\Generator $tokens, ?string $endDelimiter): array
+    {
+        $result = [];
+        do {
+            list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+            if ($delimiter == $endDelimiter) {
+                // Add an empty element only if there was a comma
+                if (!$empty || $result) $result[] = $value;
+                $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
+                );
+            } else {
+                $result[] = $value;
+            }
+        } 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];
+    }
+
+
+    protected function parseDictionaryImpl(\Generator $tokens, ?string $endDelimiter): array
+    {
+        $result = [];
+        do {
+            $value = null;
+            list($key, $delimiter) = $tokens->current();
+            $key = strval($key);
+            $tokens->next();
+            if ($delimiter == $this->delimiters[self::KVSEP]) {
+                if ($tokens->valid()) {
+                    list($value, $empty, $delimiter) = $this->parseImpl($tokens);
+                } else {
+                    $delimiter = null;
+                    $empty = true;
+                }
+                $result[$key] = $empty ? (
+                    $this->options['key-is-value'] ? $key : $this->options['empty']
+                ) : $value;
+            } elseif ($key) {
+                $result[$key] = $this->options['key-is-value'] ? $key : $this->options['empty'];
+            }
+            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];
+    }
+
+}
\ No newline at end of file
diff --git a/extension/Classes/Core/Parser/SimpleParser.php b/extension/Classes/Core/Parser/SimpleParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..20200c385b327e94d090463a2fda73d86304f9e9
--- /dev/null
+++ b/extension/Classes/Core/Parser/SimpleParser.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Represents a number prefixed with a sign. This allows to treat such values
+ * differently from absolute numbers.
+ * @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;
+    }
+}
+
+/**
+ * A basic parser that splits the provided string at unescaped and unquoted delimiters.
+ * Token processing recognizes numbers and specials values.
+ */
+class SimpleParser extends StringTokenizer
+{
+    const OPTION_KEEP_SIGN = 'keep-sign';
+    const OPTION_KEY_IS_VALUE = 'key-is-value';
+    const OPTION_EMPTY_VALUE = 'empty';
+    const OPTION_PARSE_NUMBERS = 'parse-numbers';
+
+    public array $specialValues = [
+        'null' => null,
+        'false' => false,
+        'true' => true,
+        'yes' => true,
+        'no' => false
+    ];
+
+    /** @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
+    ];
+    
+    public function __construct(string $delimiters, array $options = [])
+    {
+        parent::__construct($delimiters);
+        $this->options = array_merge($this->options, $options);
+    }
+
+    /**
+     * Processes a token into a string, number, or 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)) {
+            return $this->specialValues[$asString];
+        } else {
+            return $asString;
+        }
+    }
+
+
+    protected static function raiseUnexpectedDelimiter($delimiter, $position, $expected=null)
+    {
+        $msg = "An unexpected '$delimiter' at position $position";
+        if ($expected) {
+            $msg .= " while expecting " . implode(' or ', str_split($expected));
+        }
+        throw new \RuntimeException($msg);
+    }
+
+    protected static function raiseUnexpectedEnd($expected)
+    {
+        throw new \RuntimeException("An unexpected end while searching for '$expected'");
+    }
+
+    protected static function raiseUnexpectedToken($position, $before, $after)
+    {
+        $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]);
+        }
+    }}
\ No newline at end of file
diff --git a/extension/Classes/Core/Parser/StringTokenizer.php b/extension/Classes/Core/Parser/StringTokenizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..63decd8c2e79569d81a3cf7c1a5e3bbac01151d2
--- /dev/null
+++ b/extension/Classes/Core/Parser/StringTokenizer.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * Class StringTokenizer
+ * @project qfq
+ */
+class StringTokenizer
+{
+    private string $delimitersPattern;
+
+    protected TokenBuilder $tokenBuilder;
+
+    private int $currentOffset = 0;
+
+    public function offset(): int {
+        return $this->currentOffset-1;
+    }
+
+    public function __construct(string $delimiters)
+    {
+        $escapedDelimiters = str_replace(
+            ['[', ']', '/', '.'],
+            ['\\[', '\\]', '\\/', '\\.'],
+            $delimiters
+        );
+        $this->delimitersPattern = "/[$escapedDelimiters\\\\\"']/";
+        $this->tokenBuilder = new TokenBuilder();
+    }
+
+    /**
+     * Iterates over unescaped delimiters and quotes from a provided string.
+     * At each iteration returns a triple consisting of
+     * - the substring bound by the previous and current delimiter
+     * - the delimiter
+     * - the offset of the delimiter (mostly for errors and statistics)
+     * The delimiter and offset are null when the end of the string is reached.
+     *
+     * @param string $data   the searched string
+     * @return \Generator    triples [substring, delimiter, offset]
+     */
+    protected function unescapedDelimitersAndQuotes(string $data): \Generator
+    {
+        $this->tokenBuilder->reset();
+        $this->currentOffset = 0;
+        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 removed.
+     * At each iteration returns a triple consisting of
+     * - the substring bound by the previous and current delimiter
+     * - the delimiter
+     * - the offset of the delimiter (mostly for errors and statistics)
+     * The delimiter and offset are null when the end of the string is reached.
+     *
+     * @param string $data   the string to search for delimiters
+     * @return \Generator    triples [token, delimiter, offset]
+     */
+    public function tokenized(string $data): \Generator
+    {
+        $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) {
+                    $delimitersAndQuotes->next();
+                    if (!$delimitersAndQuotes->valid()) {
+                        throw new \RuntimeError("An unexpected end while searching for '$delimiter'");
+                    }
+                    $delimiter = $delimitersAndQuotes->current();
+                    if ($delimiter === $quote) {
+                        $this->tokenBuilder->markQuote();
+                        break;
+                    }
+                    $this->tokenBuilder->pieces[] = $delimiter;
+                };
+            } else {
+                // An unescaped delimiter: return the current token
+                // and prepare to build 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..aaed7b23fbb363e583eb9e8c6cefbf6da6c78ff6
--- /dev/null
+++ b/extension/Classes/Core/Parser/Token.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A token returned by StringTokenizer when parsing a string.
+ * It represents a part of the 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;
+
+    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..f52db40c157b3aa641add559baee95b89d668eb6
--- /dev/null
+++ b/extension/Classes/Core/Parser/TokenBuilder.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace IMATHUZH\Qfq\Core\Parser;
+
+/**
+ * A helper class used by StringTokenizer to build a token.
+ * @package qfq
+ */
+class TokenBuilder
+{
+    public int $firstQuoteOffset = -1;
+    public int $lastQuoteOffset = -1;
+
+    /**
+     * @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 = [];
+
+    public int $length = 0;
+
+    /**
+     * 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->firstQuoteOffset = -1;
+        $this->lastQuoteOffset = -1;
+        $this->length = 0;
+    }
+
+    /**
+     * Processes the data to a token and resets the builder
+     * @return Token
+     */
+    public function process(): Token
+    {
+        $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;
+    }
+
+    public function append(string $data)
+    {
+        $this->pieces[] = $data;
+        $this->length += strlen($data);
+    }
+
+    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 c5dad53c847bcc7ace1a5be6d0b1507dcf12cfe1..52c4df00174f6e2107113c48c839929bf61d1098 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';
@@ -1341,6 +1346,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..2ee2914ece99a6df9d4a0f618125f49415e4afb2
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/KVPairListParserTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Unit\Core\Parser;
+
+use IMATHUZH\Qfq\Core\Parser\KVPairListParser;
+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('|', ':', ['key-is-value' => true]);
+        $this->assertEquals(['a' => 'a'], $parser->parse('a:'));
+
+        $parser = new KVPairListParser('|', ':', ['key-is-value' => false, 'empty' => 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..7e921f53be7948bc09998c7c36d139beec1e9126
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/MixedTypeParserTest.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: megger
+ * Date: 12/17/18
+ * Time: 9:14 AM
+ */
+
+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]}')
+        );
+    }
+
+    public function testJWT()
+    {
+        $parser = new MixedTypeParser();
+        $payload = $parser->parseDictionary("exp:16161616, txt:Hello world!");
+        $this->assertArrayHasKey('exp', $payload);
+        $this->assertArrayHasKey('txt', $payload);
+    }
+}
\ 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..2fc4a4be33f3ee7d0fff65599a031f7e4917e0a9
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/SimpleParserTest.php
@@ -0,0 +1,76 @@
+<?php
+
+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..4aa77a0704e2350629cdd824846094d454896189
--- /dev/null
+++ b/extension/Tests/Unit/Core/Parser/StringTokenizerTest.php
@@ -0,0 +1,145 @@
+<?php
+
+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']
+        );
+    }
+
+}
\ No newline at end of file
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"