diff --git a/.gitignore b/.gitignore index 57a1574..7a505a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,196 +1,11 @@ -## 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 - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ [Oo]bj/ - -# Visual Studo 2015 cache/options directory +[Bb]in/ +.nuget/ +packages/ +artifacts/ .vs/ - -# 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 - -*_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 -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding addin-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# 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 - -# 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 - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -*.[Cc]ache -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -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/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt +debugSettings.json +project.lock.json +*.user +*.suo +nuget.exe diff --git a/Jwt.NET.sln b/Jwt.NET.sln new file mode 100644 index 0000000..f874c89 --- /dev/null +++ b/Jwt.NET.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.24720.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{27CE9B86-3C2B-4DC5-A4A4-9B7CE5B19FD1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EB12BB02-C15C-469F-B4A4-6DE0E187C8DB}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "JsonWebTokens", "src\JsonWebTokens\JsonWebTokens.xproj", "{046E9A92-AECE-49B2-B33C-78CB06A15EC7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {046E9A92-AECE-49B2-B33C-78CB06A15EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {046E9A92-AECE-49B2-B33C-78CB06A15EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {046E9A92-AECE-49B2-B33C-78CB06A15EC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {046E9A92-AECE-49B2-B33C-78CB06A15EC7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {046E9A92-AECE-49B2-B33C-78CB06A15EC7} = {27CE9B86-3C2B-4DC5-A4A4-9B7CE5B19FD1} + EndGlobalSection +EndGlobal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..39983a7 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index 8ccade9..fc350f8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # Jwt.NET -.NET JSON Web Tokens implementation (including CoreCLR) +JSON Web Tokens implementation for .NET (including CoreCLR). + +## Installation + Jwt.NET is avaiable on [NuGet](https://www.nuget.org/packages/Jwt.NET). + +## Usage +### Creating tokens + +```csharp +var payload = new Dictionary() +{ + { "key1", 1 }, + { "key2", "the-value" } +}; +var secret = "SOME_SECRET_KEY"; +var token = Jwt.JsonWebToken.Encode(payload, secretKey, Jwt.JwtHashAlgorithm.HS256); +``` diff --git a/global.json b/global.json new file mode 100644 index 0000000..5ba986f --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-rc1-update1" + } +} diff --git a/src/JsonWebTokens/DefaultJsonSerializer.cs b/src/JsonWebTokens/DefaultJsonSerializer.cs new file mode 100644 index 0000000..2a7ffe3 --- /dev/null +++ b/src/JsonWebTokens/DefaultJsonSerializer.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace Jwt +{ + /// + /// implementation using Json.NET. + /// + public class DefaultJsonSerializer : IJsonSerializer + { + public string Serialize(object value) + { + return JsonConvert.SerializeObject(value); + } + + public T Deserialize(string value) + { + return JsonConvert.DeserializeObject(value); + } + } +} diff --git a/src/JsonWebTokens/IJsonSerializer.cs b/src/JsonWebTokens/IJsonSerializer.cs new file mode 100644 index 0000000..9c26105 --- /dev/null +++ b/src/JsonWebTokens/IJsonSerializer.cs @@ -0,0 +1,23 @@ +namespace Jwt +{ + /// + /// Specifies a contract for a JSON serializer implementation. + /// + public interface IJsonSerializer + { + /// + /// Serializes an object to a JSON string. + /// + /// The value to serialize. + /// A JSON string representing of the object. + string Serialize(object value); + + /// + /// Deserializes a JSON string to a typed object of type . + /// + /// The type of the object. + /// A JSON string representing the object. + /// A typed object of type . + T Deserialize(string value); + } +} diff --git a/src/JsonWebTokens/JsonWebToken.cs b/src/JsonWebTokens/JsonWebToken.cs new file mode 100644 index 0000000..282881f --- /dev/null +++ b/src/JsonWebTokens/JsonWebToken.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace Jwt +{ + public enum JwtHashAlgorithm + { + HS256, + HS384, + HS512 + } + + /// + /// Provides methods for encoding and decoding JSON Web Tokens. + /// + public static class JsonWebToken + { + /// + /// Gets or sets the implementation being used. + /// + public static IJsonSerializer JsonSerializer = new DefaultJsonSerializer(); + + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Creates a JWT using the specified payload, key and algorithm. + /// + /// An arbitrary payload (must be serializable to JSON). + /// The key bytes used to sign the token. + /// The hash algorithm to use. + /// The generated JWT. + public static string Encode(object payload, byte[] key, JwtHashAlgorithm algorithm) + { + return Encode(new JwtData { Payload = payload, KeyBytes = key, Algorithm = algorithm }); + } + + /// + /// Creates a JWT using the specified payload, key and algorithm. + /// + /// An arbitrary payload (must be serializable to JSON). + /// The key used to sign the token. + /// The hash algorithm to use. + /// The generated JWT. + public static string Encode(object payload, string key, JwtHashAlgorithm algorithm) + { + return Encode(new JwtData { Payload = payload, Key = key, Algorithm = algorithm }); + } + + /// + /// Creates a JWT using the specified . + /// + /// A object. + /// The generated JWT. + public static string Encode(JwtData data) + { + var header = new Dictionary(data.ExtraHeaders) + { + { "typ", "JWT" }, + { "alg", data.Algorithm.ToString() } + }; + + var headerBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header)); + var payloadBytes = Encoding.UTF8.GetBytes(data.Serialized ? (string)data.Payload : JsonSerializer.Serialize(data.Payload)); + + var segments = new List + { + Base64UrlEncode(headerBytes), + Base64UrlEncode(payloadBytes) + }; + + var bytesToSign = Encoding.UTF8.GetBytes(string.Join(".", segments)); + + var keyBytes = data.KeyBytes; + if (keyBytes == null || keyBytes.Length == 0) + { + keyBytes = Encoding.UTF8.GetBytes(data.Key); + } + + var signature = ComputeHash(data.Algorithm, keyBytes, bytesToSign); + segments.Add(Base64UrlEncode(signature)); + + return string.Join(".", segments); + } + + /// + /// Given a JWT, decode it and return the JSON payload. + /// + /// The JWT. + /// The key that was used to sign the JWT. + /// Whether to verify the signature (default is true). + /// A string containing the JSON payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static string Decode(string token, string key, bool verify = true) + { + return Decode(token, Encoding.UTF8.GetBytes(key), verify); + } + + /// + /// Decodes the specified JWT and returns the JSON payload. + /// + /// The JWT. + /// The key bytes that were used to sign the JWT. + /// Whether to verify the signature (default is true). + /// A string containing the JSON payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static string Decode(string token, byte[] key, bool verify = true) + { + var parts = token.Split('.'); + if (parts.Length != 3) + { + throw new ArgumentException($"Token must consist of 3 parts delimited by dot. Given token: '{token}'."); + } + + var header = parts[0]; + var payload = parts[1]; + var crypto = Base64UrlDecode(parts[2]); + + var headerBytes = Base64UrlDecode(header); + var headerJson = Encoding.UTF8.GetString(headerBytes, 0, headerBytes.Length); + + var payloadBytes = Base64UrlDecode(payload); + var payloadJson = Encoding.UTF8.GetString(payloadBytes, 0, payloadBytes.Length); + + var headerData = JsonSerializer.Deserialize>(headerJson); + + if (verify) + { + var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload)); + var algorithm = (string)headerData["alg"]; + + var signature = ComputeHash(GetHashAlgorithm(algorithm), key, bytesToSign); + var decodedCrypto = Convert.ToBase64String(crypto); + var decodedSignature = Convert.ToBase64String(signature); + + Verify(decodedCrypto, decodedSignature, payloadJson); + } + + return payloadJson; + } + + /// + /// Decodes the JWT token and deserializes JSON payload to an . + /// + /// The JWT. + /// The key that was used to sign the JWT. + /// Whether to verify the signature (default is true). + /// An object representing the payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static object DecodeToObject(string token, string key, bool verify = true) + { + return DecodeToObject(token, Encoding.UTF8.GetBytes(key), verify); + } + + /// + /// Decodes the JWT token and deserializes JSON payload to an . + /// + /// The JWT. + /// The key that was used to sign the JWT. + /// Whether to verify the signature (default is true). + /// An object representing the payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static object DecodeToObject(string token, byte[] key, bool verify = true) + { + var payloadJson = Decode(token, key, verify); + var payloadData = JsonSerializer.Deserialize>(payloadJson); + return payloadData; + } + + /// + /// Decodes the JWT token and deserializes JSON payload to . + /// + /// The type of the object. + /// The JWT. + /// The key that was used to sign the JWT. + /// Whether to verify the signature (default is true). + /// An object representing the payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static T DecodeToObject(string token, string key, bool verify = true) + { + return DecodeToObject(token, Encoding.UTF8.GetBytes(key), verify); + } + + /// + /// Decodes the JWT token and deserializes JSON payload to . + /// + /// The to return + /// The JWT. + /// The key that was used to sign the JWT. + /// Whether to verify the signature (default is true). + /// An object representing the payload. + /// + /// If the parameter was true and the signature was not valid + /// or if the JWT was signed with an unsupported algorithm. + /// + /// + /// When the given doesn't consist of 3 parts delimited by dots. + /// + public static T DecodeToObject(string token, byte[] key, bool verify = true) + { + var payloadJson = Decode(token, key, verify); + var payloadData = JsonSerializer.Deserialize(payloadJson); + return payloadData; + } + + private static void Verify(string decodedCrypto, string decodedSignature, string payloadJson) + { + if (decodedCrypto != decodedSignature) + { + throw new SignatureVerificationException($"Invalid signature. Expected '{decodedCrypto}' got '{decodedSignature}'."); + } + + // Verify exp claim: https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.4 + var payloadData = JsonSerializer.Deserialize>(payloadJson); + if (payloadData.ContainsKey("exp") && payloadData["exp"] != null) + { + // Safely unpack a boxed int. + int exp; + try + { + exp = Convert.ToInt32(payloadData["exp"]); + } + catch (Exception) + { + throw new SignatureVerificationException($"Claim 'exp' must be an integer. Given claim: '{payloadData["exp"]}'."); + } + + var secondsSinceEpoch = Math.Round((DateTime.UtcNow - UnixEpoch).TotalSeconds); + if (secondsSinceEpoch >= exp) + { + throw new SignatureVerificationException("Token has expired."); + } + } + } + + private static byte[] ComputeHash(JwtHashAlgorithm algorithm, byte[] key, byte[] value) + { + HashAlgorithm hashAlgorithm; + switch (algorithm) + { + case JwtHashAlgorithm.HS256: + hashAlgorithm = new HMACSHA256(key); + break; + case JwtHashAlgorithm.HS384: + hashAlgorithm = new HMACSHA384(key); + break; + case JwtHashAlgorithm.HS512: + hashAlgorithm = new HMACSHA512(key); + break; + default: + throw new Exception($"Unsupported hash algorithm: '{algorithm}'."); + } + + using (hashAlgorithm) + { + return hashAlgorithm.ComputeHash(value); + } + } + + private static JwtHashAlgorithm GetHashAlgorithm(string algorithm) + { + switch (algorithm) + { + case "HS256": + return JwtHashAlgorithm.HS256; + case "HS384": + return JwtHashAlgorithm.HS384; + case "HS512": + return JwtHashAlgorithm.HS512; + default: + throw new SignatureVerificationException("Algorithm not supported."); + } + } + + public static string Base64UrlEncode(byte[] input) + { + var output = Convert.ToBase64String(input); + output = output.Split('=')[0]; // Remove any trailing '='s + output = output.Replace('+', '-'); // 62nd char of encoding + output = output.Replace('/', '_'); // 63rd char of encoding + return output; + } + + public static byte[] Base64UrlDecode(string input) + { + var output = input; + output = output.Replace('-', '+'); // 62nd char of encoding + output = output.Replace('_', '/'); // 63rd char of encoding + switch (output.Length % 4) // Pad with trailing '='s + { + case 0: + break; // No pad chars in this case + case 2: + output += "=="; + break; // Two pad chars + case 3: + output += "="; + break; // One pad char + default: + throw new Exception($"Illegal base64url string: '{input}'."); + } + + var converted = Convert.FromBase64String(output); + return converted; + } + } +} diff --git a/src/JsonWebTokens/JsonWebTokens.xproj b/src/JsonWebTokens/JsonWebTokens.xproj new file mode 100644 index 0000000..8edfc48 --- /dev/null +++ b/src/JsonWebTokens/JsonWebTokens.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 046e9a92-aece-49b2-b33c-78cb06a15ec7 + Jwt + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/JsonWebTokens/JwtData.cs b/src/JsonWebTokens/JwtData.cs new file mode 100644 index 0000000..b141cfd --- /dev/null +++ b/src/JsonWebTokens/JwtData.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; + +namespace Jwt +{ + public class JwtData + { + public byte[] KeyBytes { get; set; } + + public string Key { get; set; } + + public object Payload { get; set; } + + public JwtHashAlgorithm Algorithm { get; set; } = JwtHashAlgorithm.HS256; + + /// + /// Gets or sets a value whether the payload is already serialized to JSON. + /// + public bool Serialized { get; set; } + + public IDictionary ExtraHeaders { get; set; } + } + + public class JwtBuilder + { + private readonly JwtData _data = new JwtData(); + + public JwtBuilder WithPayload(object payload) + { + _data.Payload = payload; + return this; + } + + public JwtBuilder IsSerialized() + { + _data.Serialized = true; + return this; + } + + public JwtBuilder WithKey(string key) + { + _data.Key = key; + return this; + } + + public JwtBuilder WithKey(byte[] keyBytes) + { + _data.KeyBytes = keyBytes; + return this; + } + + public JwtBuilder WithAlgorithm(JwtHashAlgorithm algorithm) + { + _data.Algorithm = algorithm; + return this; + } + + public JwtBuilder WithHeader(string key, object value) + { + if (_data.ExtraHeaders == null) + { + _data.ExtraHeaders = new Dictionary(); + } + + _data.ExtraHeaders.Add(key, value); + return this; + } + + public JwtBuilder WithHeaders(IDictionary dict) + { + _data.ExtraHeaders = dict; + return this; + } + + /// + /// Builds the data to a object. + /// + /// A object. + public JwtData Build() + { + return _data; + } + } +} diff --git a/src/JsonWebTokens/SignatureVerificationException.cs b/src/JsonWebTokens/SignatureVerificationException.cs new file mode 100644 index 0000000..7c1af0b --- /dev/null +++ b/src/JsonWebTokens/SignatureVerificationException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Jwt +{ + public class SignatureVerificationException : Exception + { + public SignatureVerificationException(string message) : base(message) + { + } + } +} diff --git a/src/JsonWebTokens/project.json b/src/JsonWebTokens/project.json new file mode 100644 index 0000000..51b6348 --- /dev/null +++ b/src/JsonWebTokens/project.json @@ -0,0 +1,28 @@ +{ + "version": "1.0.0-beta-1", + "title": "Jwt.NET", + "description": "JSON Web Tokens implementation for .NET", + "authors": [ "Henk Mollema" ], + "owners": [ "Henk Mollema" ], + "tags": [ "jwt", "tokens", ".net" ], + "projectUrl": "https://github.com/henkmollema/Jwt.NET", + "licenseUrl": "https://github.com/henkmollema/Jwt.NET/blob/master/LICENSE", + "copyright": "Copyright 2016 Henk Mollema", + + "dependencies": { + "Newtonsoft.Json": "8.0.1" + }, + + "frameworks": { + "net40": { }, + "net45": { }, + "net451": { }, + "dotnet5.4": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-23516", + "System.Runtime": "4.0.21-beta-23516", + "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516" + } + } + } +}