diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d2df1da5b5077d597142e85e19812ba900a4e81c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,270 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Database connection for t4 class generation from database
+**/*.generated.cs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+Resharper
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+#cake
+tools/*
+!tools/packages.config
+dist/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b339a45c43a1dcb4b80ab99e6224bc7232abd723
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,55 @@
+stages:
+  - test
+  - release
+  - releasetrigger
+
+cake:Test:
+  stage: test
+  script:
+    - PowerShell .\build.ps1 -Target Test -Configuration Debug
+  variables:
+    GIT_STRATEGY: clone
+  artifacts:
+    reports:
+      junit: "./Artifacts/TestResults.xml"
+    paths:
+      - "./Artifacts/*"
+  except:
+    - master
+    - tags
+
+cake:Release:
+  stage: release
+  script:
+    - PowerShell .\build.ps1 -Target Release -Configuration Release --nugetApiKey="${NUGET_API_KEY}"
+  variables:
+    GIT_STRATEGY: clone
+  dependencies:
+    - cake:Test
+  artifacts:
+    paths:
+      - "./Artifacts/*"
+  only:
+    - tags
+
+cake:Prerelease:
+  stage: release
+  script:
+    - PowerShell .\build.ps1 -Target Prerelease -Configuration Release
+  variables:
+    GIT_STRATEGY: clone
+  dependencies:
+    - cake:Test
+  artifacts:
+    paths:
+      - "./Artifacts/*"
+  except:
+    - tags
+    - master
+
+cake:GitlabRelease:
+    stage: releasetrigger
+    script:
+      - PowerShell .\build.ps1 -Target GitlabRelease --GitlabProjectPath="${CI_PROJECT_PATH}" --gitlabProjectId="${CI_PROJECT_ID}" --gitlabToken="${GITLAB_TOKEN}"
+    only:
+      - master
\ No newline at end of file
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1bdc0fe219f5f35d8a024a66d9fd715f44539640
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,7 @@
+mode: ContinuousDeployment
+next-version: 1.0.0
+major-version-bump-message: 'Breaking:'
+minor-version-bump-message: '(Update|New):'
+patch-version-bump-message: 'Fix:'
+no-bump-message: '.*'
+commit-message-incrementing: Enabled
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..b8cd48a4db77aeee8a31fbec89cd7c8c4fb0b5ff
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 RWTH Aachen University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/build.cake b/build.cake
new file mode 100644
index 0000000000000000000000000000000000000000..81648ff61bbee492e1bc7b4390eb8e0cc33d3056
--- /dev/null
+++ b/build.cake
@@ -0,0 +1,351 @@
+#tool nuget:?package=NUnit.ConsoleRunner&version=3.10.0
+#tool nuget:?package=vswhere&version=2.8.4
+#tool nuget:?package=GitVersion.CommandLine&version=5.1.3
+
+#addin nuget:https://api.nuget.org/v3/index.json?package=Cake.Json&version=4.0.0
+#addin nuget:https://api.nuget.org/v3/index.json?package=Newtonsoft.Json&version=11.0.2
+#addin nuget:https://api.nuget.org/v3/index.json?package=Cake.FileHelpers&version=3.2.1
+
+using System.Net;
+using System.Net.Http;
+
+// Commandline arguments
+var target = Argument("target", "Default");
+var configuration = Argument("configuration", "Release");
+var nugetApiKey = Argument<string>("nugetApiKey", null);
+var version = Argument("nugetVersion", "");
+var gitlabProjectPath = Argument("gitlabProjectPath", "");
+var gitlabProjectId = Argument("gitlabProjectId", "");
+var gitlabToken = Argument("gitlabToken", "");
+
+// Define directories
+var projects = GetFiles("./**/*.csproj");
+var artifactsDir = Directory("./Artifacts");
+string nupkgDir;
+var solutionFile = GetFiles("./**/*.sln").First();
+var projectName = solutionFile.GetFilenameWithoutExtension().ToString();
+var nugetSource = "https://api.nuget.org/v3/index.json";
+var assemblyInfoSubPath = "Properties/AssemblyInfo.cs";
+var semanticVersion = "";
+string localNugetFeed;
+
+// get latest MSBuild version
+var vsLatest  = VSWhereLatest();	
+var msBuildPathX64 = (vsLatest == null) ? null : vsLatest.CombineWithFilePath("./MSBuild/Current/Bin/MSBuild.exe");
+
+Setup(context =>{
+	nupkgDir = $"{artifactsDir.ToString()}/nupkg";
+	var branch = GitVersion(new GitVersionSettings {
+			UpdateAssemblyInfo = false
+		}).BranchName.Replace("/", "-");
+
+	localNugetFeed = $"C:\\coscine\\LocalNugetFeeds\\{branch}";
+	Information("{0}", branch);
+	Information("Started at {0}", DateTime.Now);
+});
+
+Teardown(context =>{
+	Information("Finished at {0}", DateTime.Now);
+});
+
+Task("Clean")
+.Description("Cleans all build and artifacts directories")
+.Does(() =>{
+	var settings = new DeleteDirectorySettings {
+		Recursive = true,
+		Force = true
+	};
+	
+	var directoriesToClean = new List<DirectoryPath>();
+	
+	foreach(var project in projects) {
+		directoriesToClean.Add(Directory($"{project.GetDirectory()}/obj"));
+		directoriesToClean.Add(Directory($"{project.GetDirectory()}/bin"));
+	}
+	
+	directoriesToClean.Add(artifactsDir);
+
+	foreach(var dir in directoriesToClean) {
+		Information("Cleaning {0}", dir.ToString());
+		if (DirectoryExists(dir)) {
+			DeleteDirectory(dir, settings);
+			CreateDirectory(dir);
+		} else {
+			CreateDirectory(dir);
+		}
+	}
+});
+
+Task("Restore")
+.Does(() =>{
+	NuGetRestore(solutionFile, new NuGetRestoreSettings {
+		NoCache = true,
+		FallbackSource = new List<string>{ localNugetFeed },
+	});
+});
+
+Task("Test")
+.IsDependentOn("Build")
+.Does(() =>{
+	NUnit3($"./src/**/bin/{configuration}/*.Tests.dll", new NUnit3Settings {
+		// generate the xml file
+		NoResults = false,
+		Results = new NUnit3Result[] {
+			new NUnit3Result() {
+				FileName = $"{artifactsDir}/TestResults.xml",
+				Transform = $"{Context.Environment.WorkingDirectory}/nunit3-junit.xslt"
+			}
+		}
+	});
+});
+
+Task("GitVersion")
+.Does(() => {
+	if(string.IsNullOrWhiteSpace(version)) {
+		version = GitVersion(new GitVersionSettings {
+			UpdateAssemblyInfo = false
+		}).NuGetVersionV2;
+	}
+	var index = version.IndexOf("-");
+	semanticVersion = index > 0 ? version.Substring(0, index) : version;
+	Information("Version: {0}, SemanticVersion: {1}", version, semanticVersion);
+});
+
+Task("UpdateAssemblyInfo")
+.Does(() =>{
+	var index = version.IndexOf("-");
+	var semanticVersion = index > 0 ? version.Substring(0, index) : version;
+
+	foreach(var project in projects) {
+		CreateAssemblyInfo($"{project.GetDirectory()}/{assemblyInfoSubPath}", new AssemblyInfoSettings {
+			Product = project.GetFilenameWithoutExtension().ToString(),
+			Title = project.GetFilenameWithoutExtension().ToString(),
+			Company = "IT Center, RWTH Aachen University",
+			Version = semanticVersion,
+			FileVersion = semanticVersion,
+			InformationalVersion = version,
+			Copyright = $"{DateTime.Now.Year} IT Center, RWTH Aachen University",
+			Description = $"{project.GetFilenameWithoutExtension().ToString()} is a part of the CoScInE group."
+		});
+	}
+});
+
+Task("GitlabRelease")
+.IsDependentOn("GitVersion")
+.Does(() => {
+	var client = new HttpClient();
+	client.DefaultRequestHeaders.Add("PRIVATE-TOKEN", gitlabToken);
+	
+	// get the latest tag
+	var result = client.GetAsync($"https://git.rwth-aachen.de/api/v4/projects/{gitlabProjectId}/repository/tags").Result;
+	if(!result.IsSuccessStatusCode) {
+		throw new Exception("Tag query failed.");
+	}
+
+	var tagList = result.Content.ReadAsStringAsync().Result;
+	var jArray = JArray.Parse(tagList);
+	// null if not tags exists yet
+	var lastTag = jArray.Select(x => x["name"]).FirstOrDefault();
+
+	var url = $"https://git.rwth-aachen.de/{gitlabProjectPath}";
+	
+	if(url.EndsWith(".git")) {
+		url = url.Substring(0, url.Length - ".git".Length);
+	}
+
+	if(url.EndsWith("/")) {
+		url = url.Substring(0, url.Length - 1);
+	}
+
+	var description = "";
+	// First line of description
+	// Gitlab compare url, if something can be compared
+	if(lastTag == null) {
+		description = $"# {semanticVersion} ({DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day})\n\n\n";
+	} else {
+		description = $"# [{semanticVersion}]({url}/compare/{lastTag}...v{semanticVersion}) ({DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day})\n\n\n";
+	}
+
+	// From when will messages be parsed, null results in all messages
+	var logParam = "";
+	if(lastTag != null) {
+		logParam = $"{lastTag}..Head";
+	}
+
+	Information(lastTag);
+
+	IEnumerable<string> redirectedStandardOutput;
+	var exitCodeWithArgument =
+		StartProcess(
+			"git",
+			new ProcessSettings {
+				Arguments = $"log {logParam} --pretty=format:HASH%h:%B",
+				RedirectStandardOutput = true
+			},
+			out redirectedStandardOutput
+		);
+
+	var prefixList = new Dictionary<string, List<string>>{
+		{"Fix", new List<string>()},
+		{"Update", new List<string>()},
+		{"New", new List<string>()},
+		{"Breaking", new List<string>()},
+		{"Docs", new List<string>()},
+		{"Build", new List<string>()},
+		{"Upgrade", new List<string>()},
+		{"Chore", new List<string>()},
+	};
+
+	var currentHash = "";
+	// Output last line of process output.
+	foreach(var line in redirectedStandardOutput) {
+		var commitMessage = "";
+		if(line.StartsWith("HASH")) {
+			currentHash = line.Substring("HASH".Length);
+			currentHash = currentHash.Substring(0, currentHash.IndexOf(":"));
+			commitMessage = line.Substring(currentHash.Length + line.IndexOf(currentHash) + 1);
+		} else {
+			commitMessage = line;
+		}
+
+		foreach(var kv in prefixList) {
+			if(commitMessage.StartsWith($"{kv.Key}:")) {
+				kv.Value.Add($"* {commitMessage.Substring(kv.Key.Length + 1).Trim()} {currentHash}");
+				break;
+			}
+		};
+	}
+	
+	foreach(var kv in prefixList) {
+		if(kv.Value.Any()) {
+			description += $" ### {kv.Key}\n\n";
+			foreach(var line in kv.Value) {
+				description += $"{line}\n";
+			}
+			description += "\n";
+		}
+	}
+	// correctly escape the json newlines
+	description = description.Replace("\n", "\\n");
+	Information("Description: {0}", description);
+
+	// create tag
+	result = client.PostAsync($"https://git.rwth-aachen.de/api/v4/projects/{gitlabProjectId}/repository/tags?tag_name=v{semanticVersion}&ref=master", null).Result;
+	Information("Create tag: {0}", result.Content.ReadAsStringAsync().Result);
+	if(!result.IsSuccessStatusCode) {
+		throw new Exception("Tag creation failed.");
+	}
+
+	// create release
+	var json = $"{{\"name\": \"v{semanticVersion}\", \"tag_name\": \"v{semanticVersion}\", \"description\": \"{description}\"}}";
+	var content = new StringContent(json, Encoding.UTF8, "application/json");
+	result = client.PostAsync($"https://git.rwth-aachen.de/api/v4/projects/{gitlabProjectId}/releases", content).Result;
+	Information("Create release: {0}", result.Content.ReadAsStringAsync().Result);
+	if(!result.IsSuccessStatusCode) {
+		throw new Exception("Release creation failed.");
+	}
+});
+
+Task("Build")
+.IsDependentOn("Clean")
+.IsDependentOn("GitVersion")
+.IsDependentOn("UpdateAssemblyInfo")
+.IsDependentOn("Restore")
+.Does(() =>{
+		var frameworkSettingsWindows = new MSBuildSettings {
+			Configuration = configuration
+		};
+		
+		frameworkSettingsWindows.ToolPath = msBuildPathX64;
+		frameworkSettingsWindows.WorkingDirectory = Context.Environment.WorkingDirectory;
+
+		if (configuration.Equals("Release")) {
+			frameworkSettingsWindows.WithProperty("DebugSymbols", "false");
+			frameworkSettingsWindows.WithProperty("DebugType", "None");
+		}
+
+		// Use MSBuild
+		Information("Building {0}", solutionFile);
+		MSBuild(solutionFile, frameworkSettingsWindows);
+});
+
+Task("NugetPack")
+.IsDependentOn("Build")
+.Does(() =>{
+	foreach(var project in projects) {
+			var nuspec = $"{project.GetDirectory()}/{project.GetFilenameWithoutExtension()}.nuspec";
+			if(!project.ToString().EndsWith(".Tests") && FileExists(nuspec))
+			{
+				var settings = new NuGetPackSettings 
+				{
+					OutputDirectory = nupkgDir,
+					Version = version,
+					Properties = new Dictionary<string, string>
+					{
+						{ "Configuration", configuration}
+					}
+				};				
+				NuGetPack(project.ToString(), settings);
+			}
+		}
+});
+
+Task("NugetPush")
+.IsDependentOn("NugetPack")
+.Does(() =>{
+	var nupkgs = GetFiles($"{nupkgDir}/*.nupkg");
+	Information("Need to push {0} packages", nupkgs.Count);
+	if(!String.IsNullOrWhiteSpace(nugetApiKey)) {
+		foreach(var nupkg in nupkgs) {
+			Information("Pushing {0}", nupkg);
+			NuGetPush(nupkg, new NuGetPushSettings {
+				Source = nugetSource,
+				ApiKey = nugetApiKey
+			});
+		}
+	} else {
+		Information("NugetApiKey is not set. Can't push.");
+		throw new Exception("NugetApiKey is not set. Can't push.");
+	}
+});
+
+Task("CopyToArtifacts")
+.Does(() =>{
+	foreach(var project in projects) {
+		if(!project.GetDirectory().ToString().EndsWith(".Tests")
+		&& !FileExists($"{project.GetDirectory()}/{project.GetFilenameWithoutExtension()}.nuspec")
+		&& DirectoryExists(project.GetDirectory()))
+		{
+			Information("Copying {0}/* to {1}", $"{project.GetDirectory()}/bin/{configuration}", artifactsDir);
+			CopyDirectory($"{project.GetDirectory()}/bin/{configuration}/", artifactsDir);
+		}
+	}
+});
+
+Task("NugetPushLocal")
+.IsDependentOn("NugetPack")
+.Does(() =>{	
+	var nupkgs = GetFiles($"{nupkgDir}/*.nupkg");
+	foreach(var nupkg in nupkgs) {
+		if(!DirectoryExists(localNugetFeed)) {
+			CreateDirectory(localNugetFeed);
+		}
+		CopyFile(nupkg.ToString(), $"{localNugetFeed}\\{nupkg.GetFilename()}");
+	}
+});
+
+Task("Prerelease")
+.IsDependentOn("Build")
+.IsDependentOn("CopyToArtifacts")
+.IsDependentOn("NugetPushLocal");
+
+Task("Release")
+.IsDependentOn("NugetPack")
+.IsDependentOn("CopyToArtifacts")
+.IsDependentOn("NugetPushLocal")
+.IsDependentOn("NugetPush");
+
+Task("Default")
+.IsDependentOn("Test");
+
+RunTarget(target);
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..f83382e8e4092ba6cf32a47f9dfa4e211c878430
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,255 @@
+#The MIT License (MIT)
+#
+#Copyright (c) 2014 - 2016 Patrik Svensson, Mattias Karlsson, Gary Ewan Park and contributors
+#
+#Permission is hereby granted, free of charge, to any person obtaining a copy of
+#this software and associated documentation files (the "Software"), to deal in
+#the Software without restriction, including without limitation the rights to
+#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+#the Software, and to permit persons to whom the Software is furnished to do so,
+#subject to the following conditions:
+#
+#The above copyright notice and this permission notice shall be included in all
+#copies or substantial portions of the Software.
+#
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+#FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+#COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+#IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+#CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+##########################################################################
+# This is the Cake bootstrapper script for PowerShell.
+# This file was downloaded from https://github.com/cake-build/resources
+# Feel free to change this file to fit your needs.
+##########################################################################
+
+<#
+
+.SYNOPSIS
+This is a Powershell script to bootstrap a Cake build.
+
+.DESCRIPTION
+This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
+and execute your Cake build script with the parameters you provide.
+
+.PARAMETER Script
+The build script to execute.
+.PARAMETER Target
+The build script target to run.
+.PARAMETER Configuration
+The build configuration to use.
+.PARAMETER Verbosity
+Specifies the amount of information to be displayed.
+.PARAMETER ShowDescription
+Shows description about tasks.
+.PARAMETER DryRun
+Performs a dry run.
+.PARAMETER Experimental
+Uses the nightly builds of the Roslyn script engine.
+.PARAMETER Mono
+Uses the Mono Compiler rather than the Roslyn script engine.
+.PARAMETER SkipToolPackageRestore
+Skips restoring of packages.
+.PARAMETER ScriptArgs
+Remaining arguments are added here.
+
+.LINK
+https://cakebuild.net
+
+#>
+
+[CmdletBinding()]
+Param(
+    [string]$Script = "build.cake",
+    [string]$Target,
+    [string]$Configuration,
+    [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
+    [string]$Verbosity,
+    [switch]$ShowDescription,
+    [Alias("WhatIf", "Noop")]
+    [switch]$DryRun,
+    [switch]$Experimental,
+    [switch]$Mono,
+    [switch]$SkipToolPackageRestore,
+    [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+    [string[]]$ScriptArgs
+)
+
+[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
+function MD5HashFile([string] $filePath)
+{
+    if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf))
+    {
+        return $null
+    }
+
+    [System.IO.Stream] $file = $null;
+    [System.Security.Cryptography.MD5] $md5 = $null;
+    try
+    {
+        $md5 = [System.Security.Cryptography.MD5]::Create()
+        $file = [System.IO.File]::OpenRead($filePath)
+        return [System.BitConverter]::ToString($md5.ComputeHash($file))
+    }
+    finally
+    {
+        if ($file -ne $null)
+        {
+            $file.Dispose()
+        }
+    }
+}
+
+function GetProxyEnabledWebClient
+{
+    $wc = New-Object System.Net.WebClient
+    $proxy = [System.Net.WebRequest]::GetSystemWebProxy()
+    $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials        
+    $wc.Proxy = $proxy
+    return $wc
+}
+
+Write-Host "Preparing to run build script..."
+
+if(!$PSScriptRoot){
+    $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+}
+
+$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
+$ADDINS_DIR = Join-Path $TOOLS_DIR "Addins"
+$MODULES_DIR = Join-Path $TOOLS_DIR "Modules"
+$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
+$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
+$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
+$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config"
+$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum"
+$ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config"
+$MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config"
+
+# Make sure tools folder exists
+if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
+    Write-Verbose -Message "Creating tools directory..."
+    New-Item -Path $TOOLS_DIR -Type directory | out-null
+}
+
+# Make sure that packages.config exist.
+if (!(Test-Path $PACKAGES_CONFIG)) {
+    Write-Verbose -Message "Downloading packages.config..."    
+    try {        
+        $wc = GetProxyEnabledWebClient
+        $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch {
+        Throw "Could not download packages.config."
+    }
+}
+
+# Try find NuGet.exe in path if not exists
+if (!(Test-Path $NUGET_EXE)) {
+    Write-Verbose -Message "Trying to find nuget.exe in PATH..."
+    $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) }
+    $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1
+    if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) {
+        Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)."
+        $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName
+    }
+}
+
+# Try download NuGet.exe if not exists
+if (!(Test-Path $NUGET_EXE)) {
+    Write-Verbose -Message "Downloading NuGet.exe..."
+    try {
+        $wc = GetProxyEnabledWebClient
+        $wc.DownloadFile($NUGET_URL, $NUGET_EXE)
+    } catch {
+        Throw "Could not download NuGet.exe."
+    }
+}
+
+# Save nuget.exe path to environment to be available to child processed
+$ENV:NUGET_EXE = $NUGET_EXE
+
+# Restore tools from NuGet?
+if(-Not $SkipToolPackageRestore.IsPresent) {
+    Push-Location
+    Set-Location $TOOLS_DIR
+
+    # Check for changes in packages.config and remove installed tools if true.
+    [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG)
+    if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or
+      ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) {
+        Write-Verbose -Message "Missing or changed package.config hash..."
+        Remove-Item * -Recurse -Exclude packages.config,nuget.exe
+    }
+
+    Write-Verbose -Message "Restoring tools from NuGet..."
+    $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
+
+    if ($LASTEXITCODE -ne 0) {
+        Throw "An error occurred while restoring NuGet tools."
+    }
+    else
+    {
+        $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII"
+    }
+    Write-Verbose -Message ($NuGetOutput | out-string)
+
+    Pop-Location
+}
+
+# Restore addins from NuGet
+if (Test-Path $ADDINS_PACKAGES_CONFIG) {
+    Push-Location
+    Set-Location $ADDINS_DIR
+
+    Write-Verbose -Message "Restoring addins from NuGet..."
+    $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`""
+
+    if ($LASTEXITCODE -ne 0) {
+        Throw "An error occurred while restoring NuGet addins."
+    }
+
+    Write-Verbose -Message ($NuGetOutput | out-string)
+
+    Pop-Location
+}
+
+# Restore modules from NuGet
+if (Test-Path $MODULES_PACKAGES_CONFIG) {
+    Push-Location
+    Set-Location $MODULES_DIR
+
+    Write-Verbose -Message "Restoring modules from NuGet..."
+    $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`""
+
+    if ($LASTEXITCODE -ne 0) {
+        Throw "An error occurred while restoring NuGet modules."
+    }
+
+    Write-Verbose -Message ($NuGetOutput | out-string)
+
+    Pop-Location
+}
+
+# Make sure that Cake has been installed.
+if (!(Test-Path $CAKE_EXE)) {
+    Throw "Could not find Cake.exe at $CAKE_EXE"
+}
+
+
+
+# Build Cake arguments
+$cakeArguments = @("$Script");
+if ($Target) { $cakeArguments += "-target=$Target" }
+if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
+if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
+if ($ShowDescription) { $cakeArguments += "-showdescription" }
+if ($DryRun) { $cakeArguments += "-dryrun" }
+if ($Experimental) { $cakeArguments += "-experimental" }
+if ($Mono) { $cakeArguments += "-mono" }
+$cakeArguments += $ScriptArgs
+
+# Start Cake
+Write-Host "Running build script..."
+&$CAKE_EXE $cakeArguments
+exit $LASTEXITCODE
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000000000000000000000000000000000000..d088917ed78538ff57cf654cf71aad6bf045d655
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+
+#The MIT License (MIT)
+#
+#Copyright (c) 2014 - 2016 Patrik Svensson, Mattias Karlsson, Gary Ewan Park and contributors
+#
+#Permission is hereby granted, free of charge, to any person obtaining a copy of
+#this software and associated documentation files (the "Software"), to deal in
+#the Software without restriction, including without limitation the rights to
+#use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+#the Software, and to permit persons to whom the Software is furnished to do so,
+#subject to the following conditions:
+#
+#The above copyright notice and this permission notice shall be included in all
+#copies or substantial portions of the Software.
+#
+#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+#FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+#COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+#IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+#CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+##########################################################################
+# This is the Cake bootstrapper script for Linux and OS X.
+# This file was downloaded from https://github.com/cake-build/resources
+# Feel free to change this file to fit your needs.
+##########################################################################
+
+# Define directories.
+SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+TOOLS_DIR=$SCRIPT_DIR/tools
+NUGET_EXE=$TOOLS_DIR/nuget.exe
+CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe
+PACKAGES_CONFIG=$TOOLS_DIR/packages.config
+PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum
+
+# Define md5sum or md5 depending on Linux/OSX
+MD5_EXE=
+if [[ "$(uname -s)" == "Darwin" ]]; then
+    MD5_EXE="md5 -r"
+else
+    MD5_EXE="md5sum"
+fi
+
+# Define default arguments.
+SCRIPT="build.cake"
+TARGET="Default"
+CONFIGURATION="Release"
+VERBOSITY="verbose"
+DRYRUN=
+SHOW_VERSION=false
+SCRIPT_ARGUMENTS=()
+
+# Parse arguments.
+for i in "$@"; do
+    case $1 in
+        -s|--script) SCRIPT="$2"; shift ;;
+        -t|--target) TARGET="$2"; shift ;;
+        -c|--configuration) CONFIGURATION="$2"; shift ;;
+        -v|--verbosity) VERBOSITY="$2"; shift ;;
+        -d|--dryrun) DRYRUN="-dryrun" ;;
+        --version) SHOW_VERSION=true ;;
+        --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
+        *) SCRIPT_ARGUMENTS+=("$1") ;;
+    esac
+    shift
+done
+
+# Make sure the tools folder exist.
+if [ ! -d "$TOOLS_DIR" ]; then
+  mkdir "$TOOLS_DIR"
+fi
+
+# Make sure that packages.config exist.
+if [ ! -f "$TOOLS_DIR/packages.config" ]; then
+    echo "Downloading packages.config..."
+    curl -Lsfo "$TOOLS_DIR/packages.config" https://cakebuild.net/download/bootstrapper/packages
+    if [ $? -ne 0 ]; then
+        echo "An error occurred while downloading packages.config."
+        exit 1
+    fi
+fi
+
+# Download NuGet if it does not exist.
+if [ ! -f "$NUGET_EXE" ]; then
+    echo "Downloading NuGet..."
+    curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
+    if [ $? -ne 0 ]; then
+        echo "An error occurred while downloading nuget.exe."
+        exit 1
+    fi
+fi
+
+# Restore tools from NuGet.
+pushd "$TOOLS_DIR" >/dev/null
+if [ ! -f $PACKAGES_CONFIG_MD5 ] || [ "$( cat $PACKAGES_CONFIG_MD5 | sed 's/\r$//' )" != "$( $MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' )" ]; then
+    find . -type d ! -name . | xargs rm -rf
+fi
+
+mono "$NUGET_EXE" install -ExcludeVersion
+if [ $? -ne 0 ]; then
+    echo "Could not restore NuGet packages."
+    exit 1
+fi
+
+$MD5_EXE $PACKAGES_CONFIG | awk '{ print $1 }' >| $PACKAGES_CONFIG_MD5
+
+popd >/dev/null
+
+# Make sure that Cake has been installed.
+if [ ! -f "$CAKE_EXE" ]; then
+    echo "Could not find Cake.exe at '$CAKE_EXE'."
+    exit 1
+fi
+
+# Start Cake
+if $SHOW_VERSION; then
+    exec mono "$CAKE_EXE" -version
+else
+    exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
+fi
\ No newline at end of file
diff --git a/nunit3-junit.xslt b/nunit3-junit.xslt
new file mode 100644
index 0000000000000000000000000000000000000000..bf69bdb1d2cd6fc2fdbeb915b4eb6732f720848e
--- /dev/null
+++ b/nunit3-junit.xslt
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Taken from: https://github.com/nunit/nunit-transforms/tree/master/nunit3-junit
+-->
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+  <xsl:output method="xml" indent="yes"/>
+
+  <xsl:template match="/test-run">
+    <testsuites tests="{@testcasecount}" failures="{@failed}" disabled="{@skipped}" time="{@duration}">
+      <xsl:apply-templates/>
+    </testsuites>
+  </xsl:template>
+
+  <xsl:template match="test-suite">
+    <xsl:if test="test-case">
+      <testsuite tests="{@testcasecount}" time="{@duration}" errors="{@testcasecount - @passed - @skipped - @failed}" failures="{@failed}" skipped="{@skipped}" timestamp="{@start-time}">
+        <xsl:attribute name="name">
+          <xsl:for-each select="ancestor-or-self::test-suite/@name">
+            <xsl:value-of select="concat(., '.')"/>
+          </xsl:for-each>
+        </xsl:attribute>
+        <xsl:apply-templates select="test-case"/>
+      </testsuite>
+      <xsl:apply-templates select="test-suite"/>
+    </xsl:if>
+    <xsl:if test="not(test-case)">
+      <xsl:apply-templates/>
+    </xsl:if>
+  </xsl:template>
+
+  <xsl:template match="test-case">
+    <testcase name="{@name}" assertions="{@asserts}" time="{@duration}" status="{@result}" classname="{@classname}">
+      <xsl:if test="@runstate = 'Skipped' or @runstate = 'Ignored'">
+        <skipped/>
+      </xsl:if>
+      
+      <xsl:apply-templates/>
+    </testcase>
+  </xsl:template>
+
+  <xsl:template match="command-line"/>
+  <xsl:template match="settings"/>
+
+  <xsl:template match="output">
+    <system-out>
+      <xsl:value-of select="."/>
+    </system-out>
+  </xsl:template>
+
+  <xsl:template match="stack-trace">
+  </xsl:template>
+
+  <xsl:template match="test-case/failure">
+    <failure message="{./message}">
+      <xsl:value-of select="./stack-trace"/>
+    </failure>
+  </xsl:template>
+
+  <xsl:template match="test-suite/failure"/>
+
+  <xsl:template match="test-case/reason">
+    <skipped message="{./message}"/>
+  </xsl:template>
+  
+  <xsl:template match="test-case/assertions">
+  </xsl:template>
+
+  <xsl:template match="test-suite/reason"/>
+
+  <xsl:template match="properties"/>
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/src/JwtHandler.Tests/JwtHandler.Tests.csproj b/src/JwtHandler.Tests/JwtHandler.Tests.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..c60a3215a65561182641df355a4f00a8573d467b
--- /dev/null
+++ b/src/JwtHandler.Tests/JwtHandler.Tests.csproj
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="..\packages\NUnit3TestAdapter.3.17.0\build\net35\NUnit3TestAdapter.props" Condition="Exists('..\packages\NUnit3TestAdapter.3.17.0\build\net35\NUnit3TestAdapter.props')" />
+  <Import Project="..\packages\NUnit.3.12.0\build\NUnit.props" Condition="Exists('..\packages\NUnit.3.12.0\build\NUnit.props')" />
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{09823FA2-31B4-462B-B534-956C74B56DB3}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Coscine.JwtHandler.Tests</RootNamespace>
+    <AssemblyName>Coscine.JwtHandler.Tests</AssemblyName>
+    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <Deterministic>true</Deterministic>
+    <NuGetPackageImportStamp>
+    </NuGetPackageImportStamp>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Consul, Version=0.7.2.6, Culture=neutral, PublicKeyToken=20a6ad9a81df1d95, processorArchitecture=MSIL">
+      <HintPath>..\packages\Consul.0.7.2.6\lib\net45\Consul.dll</HintPath>
+    </Reference>
+    <Reference Include="Coscine.Configuration, Version=1.5.0.0, Culture=neutral, PublicKeyToken=ce3d7a32d7dc1e5a, processorArchitecture=MSIL">
+      <HintPath>..\packages\Coscine.Configuration.1.5.0\lib\net461\Coscine.Configuration.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.JsonWebTokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.JsonWebTokens.6.7.1\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.Logging, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Logging.6.7.1\lib\net461\Microsoft.IdentityModel.Logging.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.Tokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Tokens.6.7.1\lib\net461\Microsoft.IdentityModel.Tokens.dll</HintPath>
+    </Reference>
+    <Reference Include="nunit.framework, Version=3.12.0.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
+      <HintPath>..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.IdentityModel.Tokens.Jwt, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.IdentityModel.Tokens.Jwt.6.7.1\lib\net461\System.IdentityModel.Tokens.Jwt.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http.WebRequest" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="JwtHandlerTests.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\JwtHandler\JwtHandler.csproj">
+      <Project>{f17cf42d-af36-45a4-9a81-1804e27857bd}</Project>
+      <Name>JwtHandler</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('..\packages\NUnit.3.12.0\build\NUnit.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit.3.12.0\build\NUnit.props'))" />
+    <Error Condition="!Exists('..\packages\NUnit3TestAdapter.3.17.0\build\net35\NUnit3TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\NUnit3TestAdapter.3.17.0\build\net35\NUnit3TestAdapter.props'))" />
+  </Target>
+</Project>
\ No newline at end of file
diff --git a/src/JwtHandler.Tests/JwtHandlerTests.cs b/src/JwtHandler.Tests/JwtHandlerTests.cs
new file mode 100644
index 0000000000000000000000000000000000000000..fb319220e34736cc7d7ed8d449c6cecb8e9fab7a
--- /dev/null
+++ b/src/JwtHandler.Tests/JwtHandlerTests.cs
@@ -0,0 +1,39 @@
+using Coscine.Configuration;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coscine.JwtHandler.Tests
+{
+    [TestFixture]
+    public class JwtHandlerTests
+    {
+
+        private JWTHandler _jwtHandler;
+
+        [OneTimeSetUp]
+        public void OneTimeSetUp()
+        {
+            _jwtHandler = new JWTHandler(new ConsulConfiguration());
+        }
+
+        [Test]
+        public void CreateAndVlidateTest()
+        {
+            var type = "TestGuid";
+            var guid = Guid.NewGuid().ToString();
+            var payload = new Dictionary<string, string> 
+            {
+                {type, guid }
+            };
+
+            var token = _jwtHandler.GenerateJwtToken(payload);
+            var contents = _jwtHandler.GetContents(token);
+
+            Assert.True(contents.Count() == 5);
+            Assert.True(contents.Where( x => x.Type == type).Count() == 1);
+            Assert.True(contents.Where(x => x.Type == type).First().Value == guid);
+        }
+    }
+}
diff --git a/src/JwtHandler.Tests/Properties/AssemblyInfo.cs b/src/JwtHandler.Tests/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000000000000000000000000000000000..76a469a1d55daac9be0cedb41863e7d2584c4be9
--- /dev/null
+++ b/src/JwtHandler.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("JwtHandler.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("JwtHandler.Tests")]
+[assembly: AssemblyCopyright("Copyright ©  2020")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("09823fa2-31b4-462b-b534-956c74b56db3")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/src/JwtHandler.Tests/packages.config b/src/JwtHandler.Tests/packages.config
new file mode 100644
index 0000000000000000000000000000000000000000..27c05e5273fa2d312574a7bf847eecb10c094d5c
--- /dev/null
+++ b/src/JwtHandler.Tests/packages.config
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Consul" version="0.7.2.6" targetFramework="net461" />
+  <package id="Coscine.Configuration" version="1.5.0" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.JsonWebTokens" version="6.7.1" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.Logging" version="6.7.1" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.Tokens" version="6.7.1" targetFramework="net461" />
+  <package id="NUnit" version="3.12.0" targetFramework="net461" />
+  <package id="NUnit3TestAdapter" version="3.17.0" targetFramework="net461" />
+  <package id="System.IdentityModel.Tokens.Jwt" version="6.7.1" targetFramework="net461" />
+</packages>
\ No newline at end of file
diff --git a/src/JwtHandler.sln b/src/JwtHandler.sln
new file mode 100644
index 0000000000000000000000000000000000000000..59f3db381ef30dde310fb8f05d10315a7fa2a1c1
--- /dev/null
+++ b/src/JwtHandler.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30320.27
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JwtHandler", "JwtHandler\JwtHandler.csproj", "{F17CF42D-AF36-45A4-9A81-1804E27857BD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JwtHandler.Tests", "JwtHandler.Tests\JwtHandler.Tests.csproj", "{09823FA2-31B4-462B-B534-956C74B56DB3}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{F17CF42D-AF36-45A4-9A81-1804E27857BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F17CF42D-AF36-45A4-9A81-1804E27857BD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F17CF42D-AF36-45A4-9A81-1804E27857BD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F17CF42D-AF36-45A4-9A81-1804E27857BD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{09823FA2-31B4-462B-B534-956C74B56DB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{09823FA2-31B4-462B-B534-956C74B56DB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{09823FA2-31B4-462B-B534-956C74B56DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{09823FA2-31B4-462B-B534-956C74B56DB3}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {C10531DF-1BB5-47AE-87B3-B473BB57B124}
+	EndGlobalSection
+EndGlobal
diff --git a/src/JwtHandler/JwtHandler.cs b/src/JwtHandler/JwtHandler.cs
new file mode 100644
index 0000000000000000000000000000000000000000..eea1926c81bf61479382ce703dbaa060732bf36c
--- /dev/null
+++ b/src/JwtHandler/JwtHandler.cs
@@ -0,0 +1,89 @@
+using Coscine.Configuration;
+using Microsoft.IdentityModel.Tokens;
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+
+namespace Coscine.JwtHandler
+{
+    public class JWTHandler
+    {
+        public IConfiguration Configuration { get; set; }
+        private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler;
+        private readonly SymmetricSecurityKey _symmetricSecurityKey;
+
+        public JWTHandler(IConfiguration configuration)
+        {
+            Configuration = configuration;
+            _jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
+            _symmetricSecurityKey = GetSecurityKey();
+        }
+
+        public SymmetricSecurityKey GetSecurityKey()
+        {
+            string secretKey = Configuration.GetStringAndWait("coscine/global/jwtsecret");
+
+            if (secretKey == null)
+            {
+                throw new ArgumentNullException("JWT Secret Configuration value is not set!");
+            }
+
+            return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
+        }
+
+        public bool IsTokenValid(string token)
+        {
+            var tokenValidationParameters = new TokenValidationParameters
+            {
+                ValidateIssuerSigningKey = true,
+                IssuerSigningKey = _symmetricSecurityKey,
+                // TODO: Validate those two
+                ValidateAudience = false,
+                ValidateIssuer = false
+            };
+
+            _jwtSecurityTokenHandler.ValidateToken(token, tokenValidationParameters, out _);
+            return true;
+        }
+
+        public IEnumerable<Claim> GetContents(string jwt)
+        {
+            return _jwtSecurityTokenHandler.ReadJwtToken(jwt).Claims;
+        }
+
+        public string GenerateJwtToken(JwtPayload payload, string signatureAlgorithm = "HS256")
+        {
+            if (payload == null)
+            {
+                throw new ArgumentNullException("JwtPayload value is not set!");
+            }
+
+            var signingCredentials = new SigningCredentials(_symmetricSecurityKey, signatureAlgorithm);
+
+            var centuryBegin = new DateTime(1970, 1, 1);
+            var exp = new TimeSpan(DateTime.Now.AddMinutes(30).Ticks - centuryBegin.Ticks).TotalSeconds;
+            var now = new TimeSpan(DateTime.Now.Ticks - centuryBegin.Ticks).TotalSeconds;
+
+            payload.Add("iss", "coscine");
+            payload.Add("aud", "coscine");
+            payload.Add("iat", (long)now);
+            payload.Add("exp", (long)exp);
+
+            var header = new JwtHeader(signingCredentials);
+            var securityToken = new JwtSecurityToken(header, payload);
+
+            return _jwtSecurityTokenHandler.WriteToken(securityToken);
+        }
+
+        public string GenerateJwtToken(IReadOnlyDictionary<string, string> payloadContents, string signatureAlgorithm = "HS256")
+        {
+            var payload = new JwtPayload(payloadContents.Select(c => new Claim(c.Key, c.Value)));
+
+            return GenerateJwtToken(payload, signatureAlgorithm);
+        }
+
+    }
+}
diff --git a/src/JwtHandler/JwtHandler.csproj b/src/JwtHandler/JwtHandler.csproj
new file mode 100644
index 0000000000000000000000000000000000000000..3751c4164b1254831d1d96c504284cfa06ebe5bd
--- /dev/null
+++ b/src/JwtHandler/JwtHandler.csproj
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{F17CF42D-AF36-45A4-9A81-1804E27857BD}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Coscine.JwtHandler</RootNamespace>
+    <AssemblyName>Coscine.JwtHandler</AssemblyName>
+    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <Deterministic>true</Deterministic>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Consul, Version=0.7.2.6, Culture=neutral, PublicKeyToken=20a6ad9a81df1d95, processorArchitecture=MSIL">
+      <HintPath>..\packages\Consul.0.7.2.6\lib\net45\Consul.dll</HintPath>
+    </Reference>
+    <Reference Include="Coscine.Configuration, Version=1.5.0.0, Culture=neutral, PublicKeyToken=ce3d7a32d7dc1e5a, processorArchitecture=MSIL">
+      <HintPath>..\packages\Coscine.Configuration.1.5.0\lib\net461\Coscine.Configuration.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.JsonWebTokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.JsonWebTokens.6.7.1\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.Logging, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Logging.6.7.1\lib\net461\Microsoft.IdentityModel.Logging.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.Tokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Tokens.6.7.1\lib\net461\Microsoft.IdentityModel.Tokens.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.IdentityModel.Tokens.Jwt, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\System.IdentityModel.Tokens.Jwt.6.7.1\lib\net461\System.IdentityModel.Tokens.Jwt.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http.WebRequest" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="JwtHandler.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>
\ No newline at end of file
diff --git a/src/JwtHandler/JwtHandler.nuspec b/src/JwtHandler/JwtHandler.nuspec
new file mode 100644
index 0000000000000000000000000000000000000000..60e579497699e89c662e937b0e51a0940139a387
--- /dev/null
+++ b/src/JwtHandler/JwtHandler.nuspec
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<package >
+  <metadata>
+    <id>$id$</id>
+    <version>$version$</version>
+    <title>$title$</title>
+    <authors>rwth-aachen</authors>
+    <owners>rwth-aachen</owners>
+    <license type="expression">MIT</license>
+    <projectUrl>https://git.rwth-aachen.de/coscine/cs/jwt-handler</projectUrl>
+    <requireLicenseAcceptance>false</requireLicenseAcceptance>
+    <description>$description$</description>
+    <copyright>$copyright$</copyright>
+  </metadata>
+</package>
\ No newline at end of file
diff --git a/src/JwtHandler/Properties/AssemblyInfo.cs b/src/JwtHandler/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000000000000000000000000000000000..edc1e9a2e4ba049a1306d275b8ac105581f7126e
--- /dev/null
+++ b/src/JwtHandler/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("JwtHandler")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("JwtHandler")]
+[assembly: AssemblyCopyright("Copyright ©  2020")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f17cf42d-af36-45a4-9a81-1804e27857bd")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/src/JwtHandler/packages.config b/src/JwtHandler/packages.config
new file mode 100644
index 0000000000000000000000000000000000000000..a9a45910dbe74b8325f9672b761ae1b35ca943a5
--- /dev/null
+++ b/src/JwtHandler/packages.config
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Consul" version="0.7.2.6" targetFramework="net461" />
+  <package id="Coscine.Configuration" version="1.5.0" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.JsonWebTokens" version="6.7.1" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.Logging" version="6.7.1" targetFramework="net461" />
+  <package id="Microsoft.IdentityModel.Tokens" version="6.7.1" targetFramework="net461" />
+  <package id="System.IdentityModel.Tokens.Jwt" version="6.7.1" targetFramework="net461" />
+</packages>
\ No newline at end of file
diff --git a/tools/packages.config b/tools/packages.config
new file mode 100644
index 0000000000000000000000000000000000000000..14aef3bf00189e621993b61ccea2e4a203e73b9b
--- /dev/null
+++ b/tools/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+    <package id="Cake" version="0.36.0" />
+</packages>