Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Support for audience, issuer and expiration check + unit tests #3

Closed
wants to merge 1 commit into from

2 participants

@woloski

Added support for

  • Audience check (DecodeToObject(jwt, secret, audience: "foo")
  • Expiration check (DecodeToObject(jwt, secret, checkExpiration: true) default: false
  • Issuer check (DecodeToObject(jwt, secret, issuer: "bar")

I kept it backward compat but added an override that takes the secret as a byte[] instead of a string. This is a common practice, since you don't know how those bytes were encoded (you are currently assuming UTF8), specially in interop scenario. All the checks are done optionally and are turned off by default as well.

Let me know if this is ok and if you can publish latest to NuGet so I can take the dependency.

@mikelehen mikelehen commented on the diff
JWT/JWT.cs
((169 lines not shown))
+ {
+ private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;
+ private static JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
+
+ static JsonWebToken()
+ {
+ HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
+ {
+ { JwtHashAlgorithm.HS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
+ { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
+ { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
+ };
+ }
+
+ /// <summary>
+ /// Creates a JWT given a payload, the signing key (as a string that will be decoded with UTF8), and the algorithm to use.
@mikelehen Owner

The string will be encoded as UTF8, not decoded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mikelehen mikelehen commented on the diff
README.md
@@ -11,7 +11,7 @@ The easiest way to install is via NuGet. See [here](https://nuget.org/packages/
{ "claim1", 0 },
{ "claim2", "claim2-value" }
};
- var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
+ var secretKey = Convert.FromBase64String("GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk");
@mikelehen Owner

This shouldn't be Base64-decoded (it'll make the output below wrong and it also complicates the example).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mikelehen
Owner

Thanks for the pull request. Unfortunately, I'm not quite happy with it. I think the claim-checking is probably best done by the application code, not inside the JWT library. The JWT spec mentions, "The interpretation of the audience value is generally application specific." and I assumed issuer was mostly informational, not something you'd generally want to validate. Since these are just string comparisons, it's no easier for us to validate them internally than for the application code to handle it. Expiration time might be reasonable for us to validate internally, but even for that, the spec mentions "Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew." So I'm inclined to keep this library just focused on encoding / decoding tokens and validating their signature and leave the claim-handling to the application code.

I'm not opposed to the change that allows specifying the secret key as bytes, and the unit tests are definitely welcome. So feel free to modify the pull request to just include those changes.

Also, FYI- You seem to have converted all tabs to spaces. I'm not horribly opposed to this, but it makes it more difficult to review your changes.

@woloski

I respect your decision but not share it. Why repeat those checks over and over for each app. Also the spec says that those are optional but they still belong to the spec, hence to libraries that implement it. Anyway, it is your library so you do what you want with it. I didnt break functionallity, I built on top. It is not clear to me what you are loosing by incorporating this (people can still use the lib as it was).

@woloski

Sorry about the space/tabs. Not sure how that happened. I use tabs

@mikelehen mikelehen closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
66 JWT.Tests/JWT.Tests.csproj
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>
+ </ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{89014CFB-4C0D-4D0E-93A6-77787019C115}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>JWT.Tests</RootNamespace>
+ <AssemblyName>JWT.Tests</AssemblyName>
+ <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <TargetFrameworkProfile />
+ </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="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="JWTFixture.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\JWT\JWT.csproj">
+ <Project>{A80B51B8-DDF6-4026-98A4-B59653E50B38}</Project>
+ <Name>JWT</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
View
106 JWT.Tests/JWTFixture.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Text;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace JWT.Tests
+{
+ [TestClass]
+ public class JWTFixture
+ {
+ private static string SECRET = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
+
+ [TestMethod]
+ public void ShouldValidateToken()
+ {
+ var jwt = JWT.JsonWebToken.Encode(new { foo = "bar" }, Convert.FromBase64String(SECRET), JWT.JwtHashAlgorithm.HS256);
+ var payload = JWT.JsonWebToken.DecodeToObject(jwt, Convert.FromBase64String(SECRET)) as Dictionary<string, object>;
+
+ Assert.AreEqual("foo", payload.Keys.First());
+ Assert.AreEqual("bar", payload.Values.First());
+ }
+
+ [TestMethod]
+ public void ShouldThrowIfSignatureFails()
+ {
+ // validJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgBOo";
+ string invalidJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgB1a";
+ try
+ {
+ var payload = JWT.JsonWebToken.DecodeToObject(invalidJwt, Convert.FromBase64String(SECRET)) as Dictionary<string, object>;
+ }
+ catch (SignatureVerificationException ex)
+ {
+ Assert.AreEqual("Invalid signature. Expected 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgB1Y= got 8Qh5lJ5gSaQylkSdaCIDBoOqKzhoJ0Nutkkap8RgBOo=", ex.Message);
+ return;
+ }
+
+ Assert.Fail("Should have thrown exception");
+ }
+
+ [TestMethod]
+ public void ShouldThrowIfAudienceIsWrong()
+ {
+ var jwt = JWT.JsonWebToken.Encode(new { foo = "bar", aud = "my:audience" }, Convert.FromBase64String(SECRET), JWT.JwtHashAlgorithm.HS256);
+
+ try
+ {
+ var payload = JWT.JsonWebToken.DecodeToObject(jwt, Convert.FromBase64String(SECRET), audience: "wrong:audience") as Dictionary<string, object>;
+ }
+ catch (TokenValidationException ex)
+ {
+ Assert.AreEqual("Audience mismatch. Expected: 'wrong:audience' and got: 'my:audience'", ex.Message);
+ return;
+ }
+
+ Assert.Fail("Should have thrown exception");
+ }
+
+ [TestMethod]
+ public void ShouldThrowIfExpired()
+ {
+ var jwt = JWT.JsonWebToken.Encode(new { foo = "bar", exp = (DateTime.UtcNow.AddHours(-1).Ticks - 621355968000000000) / 10000000 }, Convert.FromBase64String(SECRET), JWT.JwtHashAlgorithm.HS256);
+
+ try
+ {
+ var payload = JWT.JsonWebToken.DecodeToObject(jwt, Convert.FromBase64String(SECRET), checkExpiration: true) as Dictionary<string, object>;
+ }
+ catch (TokenValidationException ex)
+ {
+ Assert.IsTrue(ex.Message.StartsWith("Token is expired"));
+ return;
+ }
+
+ Assert.Fail("Should have thrown exception");
+ }
+
+ [TestMethod]
+ public void ShouldNotCheckExpirationByDefault()
+ {
+ var jwt = JWT.JsonWebToken.Encode(new { foo = "bar", exp = (DateTime.UtcNow.AddHours(-1).Ticks - 621355968000000000) / 10000000 }, Convert.FromBase64String(SECRET), JWT.JwtHashAlgorithm.HS256);
+
+ var payload = JWT.JsonWebToken.DecodeToObject(jwt, Convert.FromBase64String(SECRET)) as Dictionary<string, object>;
+
+ Assert.AreEqual("bar", payload["foo"]);
+ }
+
+ [TestMethod]
+ public void ShouldThrowIfIssuerIsWrong()
+ {
+ var jwt = JWT.JsonWebToken.Encode(new { foo = "bar", iss = "some:issuer" }, Convert.FromBase64String(SECRET), JWT.JwtHashAlgorithm.HS256);
+
+ try
+ {
+ var payload = JWT.JsonWebToken.DecodeToObject(jwt, Convert.FromBase64String(SECRET), issuer: "wrong:issuer") as Dictionary<string, object>;
+ }
+ catch (TokenValidationException ex)
+ {
+ Assert.AreEqual("Token issuer mismatch. Expected: 'wrong:issuer' and got: 'some:issuer'", ex.Message);
+ return;
+ }
+
+ Assert.Fail("Should have thrown exception");
+ }
+ }
+}
View
35 JWT.Tests/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+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("JWT.Tests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("JWT.Tests")]
+[assembly: AssemblyCopyright("Copyright © 2013")]
+[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("9d3adeaa-4ca8-431c-b6ff-08b7180d1c39")]
+
+// 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.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
View
56 JWT.sln
@@ -1,20 +1,36 @@
-
-Microsoft Visual Studio Solution File, Format Version 11.00
-# Visual Studio 2010
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JWT", "JWT\JWT.csproj", "{A80B51B8-DDF6-4026-98A4-B59653E50B38}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JWT", "JWT\JWT.csproj", "{A80B51B8-DDF6-4026-98A4-B59653E50B38}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JWT.Tests", "JWT.Tests\JWT.Tests.csproj", "{89014CFB-4C0D-4D0E-93A6-77787019C115}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BB82B7E1-C553-4BA5-9BF1-AC7A79BF0E3D}"
+ ProjectSection(SolutionItems) = preProject
+ JWT.vsmdi = JWT.vsmdi
+ Local.testsettings = Local.testsettings
+ TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(TestCaseManagementSettings) = postSolution
+ CategoryFile = JWT.vsmdi
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A80B51B8-DDF6-4026-98A4-B59653E50B38}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89014CFB-4C0D-4D0E-93A6-77787019C115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89014CFB-4C0D-4D0E-93A6-77787019C115}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89014CFB-4C0D-4D0E-93A6-77787019C115}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89014CFB-4C0D-4D0E-93A6-77787019C115}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
View
6 JWT.vsmdi
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TestLists xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
+ <TestList name="Lists of Tests" id="8c43106b-9dc1-4907-a29f-aa66a61bf5b6">
+ <RunConfiguration id="bc96c989-1f36-4cd1-b6bd-24325080495f" name="Local" storage="local.testsettings" type="Microsoft.VisualStudio.TestTools.Common.TestRunConfiguration, Microsoft.VisualStudio.QualityTools.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
+ </TestList>
+</TestLists>
View
382 JWT/JWT.cs
@@ -5,157 +5,233 @@
using System.Web.Script.Serialization;
namespace JWT
-{
- public enum JwtHashAlgorithm
- {
- HS256,
- HS384,
- HS512
- }
-
- /// <summary>
- /// Provides methods for encoding and decoding JSON Web Tokens.
- /// </summary>
- public static class JsonWebToken
- {
- private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;
- private static JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
-
- static JsonWebToken()
- {
- HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
- {
- { JwtHashAlgorithm.HS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
- { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
- { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
- };
- }
-
- /// <summary>
- /// Creates a JWT given a payload, the signing key, and the algorithm to use.
- /// </summary>
- /// <param name="payload">An arbitrary payload (must be serializable to JSON via <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).</param>
- /// <param name="key">The key used to sign the token.</param>
- /// <param name="algorithm">The hash algorithm to use.</param>
- /// <returns>The generated JWT.</returns>
- public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
- {
- var segments = new List<string>();
- var header = new { typ = "JWT", alg = algorithm.ToString() };
-
- byte[] headerBytes = Encoding.UTF8.GetBytes(jsonSerializer.Serialize(header));
- byte[] payloadBytes = Encoding.UTF8.GetBytes(jsonSerializer.Serialize(payload));
-
- segments.Add(Base64UrlEncode(headerBytes));
- segments.Add(Base64UrlEncode(payloadBytes));
-
- var stringToSign = string.Join(".", segments.ToArray());
-
- var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
- var keyBytes = Encoding.UTF8.GetBytes(key);
-
- byte[] signature = HashAlgorithms[algorithm](keyBytes, bytesToSign);
- segments.Add(Base64UrlEncode(signature));
-
- return string.Join(".", segments.ToArray());
- }
-
- /// <summary>
- /// Given a JWT, decode it and return the JSON payload.
- /// </summary>
- /// <param name="token">The JWT.</param>
- /// <param name="key">The key that was used to sign the JWT.</param>
- /// <param name="verify">Whether to verify the signature (default is true).</param>
- /// <returns>A string containing the JSON payload.</returns>
- /// <exception cref="SignatureVerificationException">Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm.</exception>
- public static string Decode(string token, string key, bool verify = true)
- {
- var parts = token.Split('.');
- var header = parts[0];
- var payload = parts[1];
- byte[] crypto = Base64UrlDecode(parts[2]);
-
- var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
- var headerData = jsonSerializer.Deserialize<Dictionary<string,object>>(headerJson);
- var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
-
- if (verify)
- {
- var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
- var keyBytes = Encoding.UTF8.GetBytes(key);
- var algorithm = (string)headerData["alg"];
-
- var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
- var decodedCrypto = Convert.ToBase64String(crypto);
- var decodedSignature = Convert.ToBase64String(signature);
-
- if (decodedCrypto != decodedSignature)
- {
- throw new SignatureVerificationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
- }
- }
-
- return payloadJson;
- }
-
- /// <summary>
- /// Given a JWT, decode it and return the payload as an object (by deserializing it with <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).
- /// </summary>
- /// <param name="token">The JWT.</param>
- /// <param name="key">The key that was used to sign the JWT.</param>
- /// <param name="verify">Whether to verify the signature (default is true).</param>
- /// <returns>An object representing the payload.</returns>
- /// <exception cref="SignatureVerificationException">Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm.</exception>
- public static object DecodeToObject(string token, string key, bool verify = true)
- {
- var payloadJson = JsonWebToken.Decode(token, key, verify);
- var payloadData = jsonSerializer.Deserialize<Dictionary<string,object>>(payloadJson);
- return payloadData;
- }
-
- 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.");
- }
- }
-
- // from JWT spec
- private 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;
- }
-
- // from JWT spec
- private 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 System.Exception("Illegal base64url string!");
- }
- var converted = Convert.FromBase64String(output); // Standard base64 decoder
- return converted;
- }
- }
-
- public class SignatureVerificationException : Exception
- {
- public SignatureVerificationException(string message) : base(message)
- {
- }
- }
+{
+ public enum JwtHashAlgorithm
+ {
+ HS256,
+ HS384,
+ HS512
+ }
+
+ /// <summary>
+ /// Provides methods for encoding and decoding JSON Web Tokens.
+ /// </summary>
+ public static class JsonWebToken
+ {
+ private static Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>> HashAlgorithms;
+ private static JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
+
+ static JsonWebToken()
+ {
+ HashAlgorithms = new Dictionary<JwtHashAlgorithm, Func<byte[], byte[], byte[]>>
+ {
+ { JwtHashAlgorithm.HS256, (key, value) => { using (var sha = new HMACSHA256(key)) { return sha.ComputeHash(value); } } },
+ { JwtHashAlgorithm.HS384, (key, value) => { using (var sha = new HMACSHA384(key)) { return sha.ComputeHash(value); } } },
+ { JwtHashAlgorithm.HS512, (key, value) => { using (var sha = new HMACSHA512(key)) { return sha.ComputeHash(value); } } }
+ };
+ }
+
+ /// <summary>
+ /// Creates a JWT given a payload, the signing key (as a string that will be decoded with UTF8), and the algorithm to use.
@mikelehen Owner

The string will be encoded as UTF8, not decoded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ /// </summary>
+ /// <param name="payload">An arbitrary payload (must be serializable to JSON via <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).</param>
+ /// <param name="key">The key used to sign the token.</param>
+ /// <param name="algorithm">The hash algorithm to use.</param>
+ /// <returns>The generated JWT.</returns>
+ public static string Encode(object payload, string key, JwtHashAlgorithm algorithm)
+ {
+ return Encode(payload, Encoding.UTF8.GetBytes(key), algorithm);
+ }
+
+ /// <summary>
+ /// Creates a JWT given a payload, the signing key, and the algorithm to use.
+ /// </summary>
+ /// <param name="payload">An arbitrary payload (must be serializable to JSON via <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).</param>
+ /// <param name="key">The key used to sign the token.</param>
+ /// <param name="algorithm">The hash algorithm to use.</param>
+ /// <returns>The generated JWT.</returns>
+ public static string Encode(object payload, byte[] key, JwtHashAlgorithm algorithm)
+ {
+ var segments = new List<string>();
+ var header = new { typ = "JWT", alg = algorithm.ToString() };
+
+ byte[] headerBytes = Encoding.UTF8.GetBytes(jsonSerializer.Serialize(header));
+ byte[] payloadBytes = Encoding.UTF8.GetBytes(jsonSerializer.Serialize(payload));
+
+ segments.Add(Base64UrlEncode(headerBytes));
+ segments.Add(Base64UrlEncode(payloadBytes));
+
+ var stringToSign = string.Join(".", segments.ToArray());
+
+ var bytesToSign = Encoding.UTF8.GetBytes(stringToSign);
+
+ byte[] signature = HashAlgorithms[algorithm](key, bytesToSign);
+ segments.Add(Base64UrlEncode(signature));
+
+ return string.Join(".", segments.ToArray());
+ }
+
+ /// <summary>
+ /// Given a JWT, decode it and return the JSON payload.
+ /// </summary>
+ /// <param name="token">The JWT.</param>
+ /// <param name="key">The key that was used to sign the JWT (string that will be decoded with UTF8).</param>
+ /// <param name="verify">Whether to verify the signature (default is true).</param>
+ /// <returns>A string containing the JSON payload.</returns>
+ /// <exception cref="SignatureVerificationException">Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm.</exception>
+ public static string Decode(string token, byte[] key, bool verify = true)
+ {
+ var parts = token.Split('.');
+ var header = parts[0];
+ var payload = parts[1];
+ byte[] crypto = Base64UrlDecode(parts[2]);
+
+ var headerJson = Encoding.UTF8.GetString(Base64UrlDecode(header));
+ var headerData = jsonSerializer.Deserialize<Dictionary<string, object>>(headerJson);
+ var payloadJson = Encoding.UTF8.GetString(Base64UrlDecode(payload));
+
+ if (verify)
+ {
+ var bytesToSign = Encoding.UTF8.GetBytes(string.Concat(header, ".", payload));
+ var keyBytes = key;
+ var algorithm = (string)headerData["alg"];
+
+ var signature = HashAlgorithms[GetHashAlgorithm(algorithm)](keyBytes, bytesToSign);
+ var decodedCrypto = Convert.ToBase64String(crypto);
+ var decodedSignature = Convert.ToBase64String(signature);
+
+ if (decodedCrypto != decodedSignature)
+ {
+ throw new SignatureVerificationException(string.Format("Invalid signature. Expected {0} got {1}", decodedCrypto, decodedSignature));
+ }
+ }
+
+ return payloadJson;
+ }
+
+ /// <summary>
+ /// Given a JWT, decode it and return the payload as an object (by deserializing it with <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).
+ /// </summary>
+ /// <param name="token">The JWT.</param>
+ /// <param name="key">The key that was used to sign the JWT as a string that will be decoded with UTF8.</param>
+ /// <param name="verify">Whether to verify the signature (default is true).</param>
+ /// <param name="checkExpiration">Whether to check token expiration (exp) (default is false)</param>
+ /// <param name="audience">The expected audience (aud) of the JWT (default is null, dont check audience)</param>
+ /// <param name="issuer">The expected issuer (iss) of the JWT (default is null, dont check issuer)</param>
+ /// <returns>An object representing the payload.</returns>
+ /// <exception cref="SignatureVerificationException">Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm.</exception>
+ /// <exception cref="TokenValidationException">Thrown if audience, expiration or issuer check fails.</exception>
+ public static object DecodeToObject(string token, string key, bool verify = true, bool checkExpiration = false, string audience = null, string issuer = null)
+ {
+ return DecodeToObject(token, Encoding.UTF8.GetBytes(key), verify, checkExpiration, audience, issuer);
+ }
+
+ /// <summary>
+ /// Given a JWT, decode it and return the payload as an object (by deserializing it with <see cref="System.Web.Script.Serialization.JavaScriptSerializer"/>).
+ /// </summary>
+ /// <param name="token">The JWT.</param>
+ /// <param name="key">The key that was used to sign the JWT as a byte array.</param>
+ /// <param name="verify">Whether to verify the signature (default is true).</param>
+ /// <param name="checkExpiration">Whether to check token expiration (exp) (default is false)</param>
+ /// <param name="audience">The expected audience (aud) of the JWT (default is null, dont check audience)</param>
+ /// <param name="issuer">The expected issuer (iss) of the JWT (default is null, dont check issuer)</param>
+ /// <returns>An object representing the payload.</returns>
+ /// <exception cref="SignatureVerificationException">Thrown if the verify parameter was true and the signature was NOT valid or if the JWT was signed with an unsupported algorithm.</exception>
+ /// <exception cref="TokenValidationException">Thrown if audience, expiration or issuer check fails.</exception>
+ public static object DecodeToObject(string token, byte[] key, bool verify = true, bool checkExpiration = false, string audience = null, string issuer = null)
+ {
+ var payloadJson = JsonWebToken.Decode(token, key, verify);
+ var payloadData = jsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);
+
+ object aud;
+ if (!string.IsNullOrEmpty(audience) && payloadData.TryGetValue("aud", out aud))
+ {
+ if (!aud.ToString().Equals(audience, StringComparison.Ordinal))
+ {
+ throw new TokenValidationException(string.Format("Audience mismatch. Expected: '{0}' and got: '{1}'", audience, aud));
+ }
+ }
+
+ object exp;
+ if (checkExpiration && payloadData.TryGetValue("exp", out exp))
+ {
+ DateTime validTo = FromUnixTime(long.Parse(exp.ToString()));
+ if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0)
+ {
+ throw new TokenValidationException(string.Format("Token is expired. Expiration: '{0}'. Current: '{1}'", validTo, DateTime.UtcNow));
+ }
+ }
+
+ object iss;
+ if (!string.IsNullOrEmpty(issuer) && payloadData.TryGetValue("iss", out iss))
+ {
+ if (!iss.ToString().Equals(issuer, StringComparison.Ordinal))
+ {
+ throw new TokenValidationException(string.Format("Token issuer mismatch. Expected: '{0}' and got: '{1}'", issuer, iss));
+ }
+ }
+
+ return payloadData;
+ }
+
+ private static DateTime FromUnixTime(long unixTime)
+ {
+ var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ return epoch.AddSeconds(unixTime);
+ }
+
+ 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.");
+ }
+ }
+
+ // from JWT spec
+ private 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;
+ }
+
+ // from JWT spec
+ private 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 System.Exception("Illegal base64url string!");
+ }
+ var converted = Convert.FromBase64String(output); // Standard base64 decoder
+ return converted;
+ }
+ }
+
+ public class SignatureVerificationException : Exception
+ {
+ public SignatureVerificationException(string message)
+ : base(message)
+ {
+ }
+ }
+
+ public class TokenValidationException : Exception
+ {
+ public TokenValidationException(string message)
+ : base(message)
+ {
+ }
+ }
}
View
10 Local.testsettings
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TestSettings name="Local" id="bc96c989-1f36-4cd1-b6bd-24325080495f" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
+ <Description>These are default test settings for a local test run.</Description>
+ <Deployment enabled="false" />
+ <Execution>
+ <TestTypeSpecific />
+ <AgentRule name="Execution Agents">
+ </AgentRule>
+ </Execution>
+</TestSettings>
View
11 README.md
@@ -11,7 +11,7 @@ The easiest way to install is via NuGet. See [here](https://nuget.org/packages/
{ "claim1", 0 },
{ "claim2", "claim2-value" }
};
- var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
+ var secretKey = Convert.FromBase64String("GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk");
@mikelehen Owner

This shouldn't be Base64-decoded (it'll make the output below wrong and it also complicates the example).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
string token = JWT.JsonWebToken.Encode(payload, secretKey, JWT.JwtHashAlgorithm.HS256);
Console.Out.WriteLine(token);
@@ -22,7 +22,7 @@ Output will be:
### Verifying and Decoding Tokens
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjbGFpbTEiOjAsImNsYWltMiI6ImNsYWltMi12YWx1ZSJ9.8pwBI_HtXqI3UgQHQ_rDRnSQRxFL1SR8fbQoS-5kM5s";
- var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
+ var secretKey = Convert.FromBase64String("GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk");
try
{
string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
@@ -45,3 +45,10 @@ You can also deserialize the JSON payload directly to a .Net object with DecodeT
which will output:
claim2-value
+
+### Audience, Issuer and Expiration check
+
+The library support checking audience (aud), issuer (iss) and expiration (exp). By default these checks are turn off for backward compatibility but if you specify them, it will do the check. If the check fails it will throw a `TokenValidationException`.
+
+ var payload = JWT.JsonWebToken.DecodeToObject(token, secretKey, audience: "urn:myaudience", issuer: "my:issuer", checkExpiration: true) as IDictionary<string, object>;
+
View
21 TraceAndTestImpact.testsettings
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<TestSettings name="Trace and Test Impact" id="01d33c87-2c43-4967-8246-2b8f0618a8cb" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
+ <Description>These are test settings for Trace and Test Impact.</Description>
+ <Execution>
+ <TestTypeSpecific />
+ <AgentRule name="Execution Agents">
+ <DataCollectors>
+ <DataCollector uri="datacollector://microsoft/SystemInfo/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.DataCollection.SystemInfo.SystemInfoDataCollector, Microsoft.VisualStudio.TestTools.DataCollection.SystemInfo, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="System Information">
+ </DataCollector>
+ <DataCollector uri="datacollector://microsoft/ActionLog/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.ManualTest.ActionLog.ActionLogPlugin, Microsoft.VisualStudio.TestTools.ManualTest.ActionLog, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Actions">
+ </DataCollector>
+ <DataCollector uri="datacollector://microsoft/HttpProxy/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.HttpProxyCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="ASP.NET Client Proxy for IntelliTrace and Test Impact">
+ </DataCollector>
+ <DataCollector uri="datacollector://microsoft/TestImpact/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.TestImpactDataCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Test Impact">
+ </DataCollector>
+ <DataCollector uri="datacollector://microsoft/TraceDebugger/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TraceCollector.TraceDebuggerDataCollector, Microsoft.VisualStudio.TraceCollector, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="IntelliTrace">
+ </DataCollector>
+ </DataCollectors>
+ </AgentRule>
+ </Execution>
+</TestSettings>
Something went wrong with that request. Please try again.