diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml index 876cc11..b395eda 100644 --- a/.github/workflows/build-debug.yml +++ b/.github/workflows/build-debug.yml @@ -21,10 +21,8 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@v1 with: - dotnet-version: 2.1.x - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.1.x - + dotnet-version: | + 5.0.x + 6.0.x - run: dotnet build -c Debug - run: dotnet test -c Debug --no-build diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1f605e4..fac549a 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -14,8 +14,6 @@ on: env: GIT_TAG: ${{ github.event.inputs.tag }} DRY_RUN: ${{ github.event.inputs.dry_run }} - DOTNET_SDK_VERISON_2: 2.1.x - DOTNET_SDK_VERISON_3: 3.1.x jobs: build-dotnet: @@ -28,10 +26,9 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ env.DOTNET_SDK_VERISON_2 }} - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_SDK_VERISON_3 }} + dotnet-version: | + 5.0.x + 6.0.x # pack nuget - run: dotnet build -c Release -p:Version=${{ env.GIT_TAG }} - run: dotnet test -c Release --no-build -p:Version=${{ env.GIT_TAG }} @@ -53,7 +50,9 @@ jobs: # setup dotnet for nuget push - uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ env.DOTNET_SDK_VERISON_3 }} + dotnet-version: | + 5.0.x + 6.0.x # tag - uses: actions/checkout@v2 - name: tag diff --git a/README.md b/README.md index 87abf88..778ac65 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ LitJWT === -Lightweight, Fast [JWT(JSON Web Token)](https://jwt.io/) implementation for .NET Core. This library mainly focus on performance, 5 times faster encoding/decoding and very low allocation. +Lightweight, Fast [JWT(JSON Web Token)](https://jwt.io/) implementation for .NET. This library mainly focus on performance, 5 times faster encoding/decoding and very low allocation. ![image](https://user-images.githubusercontent.com/46207/58414904-c4c31300-80b7-11e9-9bd2-12f794518494.png) -NuGet: [LitJWT](https://www.nuget.org/packages/LitJWT), Currently only supports `.NET Core 2.1`(for performance reason, this libs uses many `Span` based API). +NuGet: [LitJWT](https://www.nuget.org/packages/LitJWT) + +Supported platform is `netstandard 2.1`, `net5.0` or greater. ``` Install-Package LitJWT @@ -40,8 +42,7 @@ var key = HS256Algorithm.GenerateRandomRecommendedKey(); var encoder = new JwtEncoder(new HS256Algorithm(key)); // Encode with payload, expire, and use specify payload serializer. -var token = encoder.Encode(new { foo = "pay", bar = "load" }, TimeSpan.FromMinutes(30), - (x, writer) => writer.Write(Utf8Json.JsonSerializer.SerializeUnsafe(x))); +var token = encoder.Encode(new { foo = "pay", bar = "load" }, TimeSpan.FromMinutes(30)); ``` ```csharp @@ -49,7 +50,7 @@ var token = encoder.Encode(new { foo = "pay", bar = "load" }, TimeSpan.FromMinut var decoder = new JwtDecoder(encoder.SignAlgorithm); // Decode and verify, you can check the result. -var result = decoder.TryDecode(token, x => Utf8Json.JsonSerializer.Deserialize(x.ToArray()), out var payload); +var result = decoder.TryDecode(token, out var payload); if (result == DecodeResult.Success) { Console.WriteLine((payload.foo, payload.bar)); @@ -58,7 +59,9 @@ if (result == DecodeResult.Success) Custom Serializer --- -Encode method receives `Action payloadWriter`. You have to invoke `writer.Write(ReadOnlySpan payload)` method to serialize. `ReadOnlySpan` must be Utf8 binary, so if you use utf8 based serializer(or Json writer) such as [Utf8Json](https://github.com/neuecc/Utf8Json), achives maximum performance. +In default. LitJWT is using `System.Text.Json.JsonSerializer`. If you want to use custom `JsonSerializerOptions`, `JwtEncoder` and `JwtDecoder` have `JsonSerializerOptions serializerOptions` constructor overload. + +If you want to use another serializer, encode method receives `Action payloadWriter`. You have to invoke `writer.Write(ReadOnlySpan payload)` method to serialize. `ReadOnlySpan` must be Utf8 binary. Here is the sample of use JSON.NET, this have encoding overhead. @@ -67,7 +70,7 @@ var token = encoder.Encode(new PayloadSample { foo = "pay", bar = "load" }, Time (x, writer) => writer.Write(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(x)))); ``` -Decode method receives `delegate T PayloadParser(ReadOnlySpan payload)`. `ReadOnlySpan` is utf8 json. Yes, utf8 based serialize is best but you can also use JSON.NET(but have encoding penalty). +Decode method receives `delegate T PayloadParser(ReadOnlySpan payload)`. `ReadOnlySpan` is utf8 json. Yes, utf8 based serializer is best but you can also use JSON.NET(but have encoding penalty). ``` var result = decoder.TryDecode(token, x => JsonConvert.DeserializeObject(Encoding.UTF8.GetString(x)), out var payload); diff --git a/sandbox/Benchmark/Benchmark.csproj b/sandbox/Benchmark/Benchmark.csproj index e34bced..c68e474 100644 --- a/sandbox/Benchmark/Benchmark.csproj +++ b/sandbox/Benchmark/Benchmark.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.2 + net6.0 diff --git a/sandbox/ConsoleApp/ConsoleApp.csproj b/sandbox/ConsoleApp/ConsoleApp.csproj index 1dd2e09..ffdebac 100644 --- a/sandbox/ConsoleApp/ConsoleApp.csproj +++ b/sandbox/ConsoleApp/ConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.2 + net6.0 diff --git a/sandbox/ConsoleApp/Program.cs b/sandbox/ConsoleApp/Program.cs index 968d10c..3ba71d0 100644 --- a/sandbox/ConsoleApp/Program.cs +++ b/sandbox/ConsoleApp/Program.cs @@ -60,7 +60,7 @@ static void Main(string[] args) var rs256 = new LitJWT.JwtEncoder(new LitJWT.Algorithms.RS256Algorithm(() => RSA.Create(rsaParams), () => RSA.Create(rsaParams))); var foo = rs256.Encode(payload, null, (x, writer) => writer.Write(Utf8Json.JsonSerializer.SerializeUnsafe(x))); - + } } diff --git a/src/LitJWT/Json/JsonReader.cs b/src/LitJWT/Json/JsonReader.cs deleted file mode 100644 index 4ccfa29..0000000 --- a/src/LitJWT/Json/JsonReader.cs +++ /dev/null @@ -1,985 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Runtime.CompilerServices; -using Utf8Json.Internal; - -// port from Utf8Json - -namespace Utf8Json -{ - // 0 = None, 1 ~ 4 is block token, 5 ~ 9 = value token, 10 ~ 11 = delimiter token - // you can use range-check if optimization needed. - - internal enum JsonToken : byte - { - None = 0, - /// { - BeginObject = 1, - /// } - EndObject = 2, - /// [ - BeginArray = 3, - /// ] - EndArray = 4, - /// 0~9, - - Number = 5, - /// " - String = 6, - /// t - True = 7, - /// f - False = 8, - /// n - Null = 9, - /// , - ValueSeparator = 10, - /// : - NameSeparator = 11 - } - - - // JSON RFC: https://www.ietf.org/rfc/rfc4627.txt - - internal ref struct JsonReader - { - static readonly ArraySegment nullTokenSegment = new ArraySegment(new byte[] { 110, 117, 108, 108 }, 0, 4); - static readonly byte[] bom = Encoding.UTF8.GetPreamble(); - - readonly ReadOnlySpan bytes; - int offset; - - public JsonReader(ReadOnlySpan bytes) - : this(bytes, 0) - { - - } - - public JsonReader(ReadOnlySpan bytes, int offset) - { - this.bytes = bytes; - this.offset = offset; - - // skip bom - if (bytes.Length >= 3) - { - if (bytes[offset] == bom[0] && bytes[offset + 1] == bom[1] && bytes[offset + 2] == bom[2]) - { - this.offset = offset += 3; - } - } - } - - JsonParsingException CreateParsingException(string expected) - { - if (bytes.Length == 0) return new JsonParsingException("expected:'" + expected + "' however buffer length is zero."); - - var actual = ((char)bytes[offset]).ToString(); - var pos = offset; - - try - { - var token = GetCurrentJsonToken(); - switch (token) - { - case JsonToken.Number: - var ns = ReadNumberSegment(); - actual = Encoding.UTF8.GetString(ns); - break; - case JsonToken.String: - //actual = "\"" + ReadStringSegmentRaw() + "\""; - break; - case JsonToken.True: - actual = "true"; - break; - case JsonToken.False: - actual = "false"; - break; - case JsonToken.Null: - actual = "null"; - break; - default: - break; - } - } - catch { } - - return new JsonParsingException("expected:'" + expected + "', actual:'" + actual + "', at offset:" + pos, pos, offset, actual); - } - - JsonParsingException CreateParsingExceptionMessage(string message) - { - var actual = ((char)bytes[offset]).ToString(); - var pos = offset; - - return new JsonParsingException(message, pos, pos, actual); - } - - bool IsInRange - { - get - { - return offset < bytes.Length; - } - } - - public void AdvanceOffset(int offset) - { - this.offset += offset; - } - - public int GetCurrentOffsetUnsafe() - { - return offset; - } - - public JsonToken GetCurrentJsonToken() - { - SkipWhiteSpace(); - if (offset < bytes.Length) - { - var c = bytes[offset]; - switch (c) - { - case (byte)'{': return JsonToken.BeginObject; - case (byte)'}': return JsonToken.EndObject; - case (byte)'[': return JsonToken.BeginArray; - case (byte)']': return JsonToken.EndArray; - case (byte)'t': return JsonToken.True; - case (byte)'f': return JsonToken.False; - case (byte)'n': return JsonToken.Null; - case (byte)',': return JsonToken.ValueSeparator; - case (byte)':': return JsonToken.NameSeparator; - case (byte)'-': return JsonToken.Number; - case (byte)'0': return JsonToken.Number; - case (byte)'1': return JsonToken.Number; - case (byte)'2': return JsonToken.Number; - case (byte)'3': return JsonToken.Number; - case (byte)'4': return JsonToken.Number; - case (byte)'5': return JsonToken.Number; - case (byte)'6': return JsonToken.Number; - case (byte)'7': return JsonToken.Number; - case (byte)'8': return JsonToken.Number; - case (byte)'9': return JsonToken.Number; - case (byte)'\"': return JsonToken.String; - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - case 32: - case 33: - case 35: - case 36: - case 37: - case 38: - case 39: - case 40: - case 41: - case 42: - case 43: - case 46: - case 47: - case 59: - case 60: - case 61: - case 62: - case 63: - case 64: - case 65: - case 66: - case 67: - case 68: - case 69: - case 70: - case 71: - case 72: - case 73: - case 74: - case 75: - case 76: - case 77: - case 78: - case 79: - case 80: - case 81: - case 82: - case 83: - case 84: - case 85: - case 86: - case 87: - case 88: - case 89: - case 90: - case 92: - case 94: - case 95: - case 96: - case 97: - case 98: - case 99: - case 100: - case 101: - case 103: - case 104: - case 105: - case 106: - case 107: - case 108: - case 109: - case 111: - case 112: - case 113: - case 114: - case 115: - case 117: - case 118: - case 119: - case 120: - case 121: - case 122: - default: - return JsonToken.None; - } - } - else - { - return JsonToken.None; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SkipWhiteSpace() - { - // eliminate array bound check - for (int i = offset; i < bytes.Length; i++) - { - switch (bytes[i]) - { - case 0x20: // Space - case 0x09: // Horizontal tab - case 0x0A: // Line feed or New line - case 0x0D: // Carriage return - continue; - case (byte)'/': // BeginComment - i = ReadComment(bytes, i); - continue; - // optimize skip jumptable - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 11: - case 12: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - case 33: - case 34: - case 35: - case 36: - case 37: - case 38: - case 39: - case 40: - case 41: - case 42: - case 43: - case 44: - case 45: - case 46: - default: - offset = i; - return; // end - } - } - - offset = bytes.Length; - } - - public bool ReadIsNull() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == 'n') - { - if (bytes[offset + 1] != 'u') goto ERROR; - if (bytes[offset + 2] != 'l') goto ERROR; - if (bytes[offset + 3] != 'l') goto ERROR; - offset += 4; - return true; - } - else - { - return false; - } - - ERROR: - throw CreateParsingException("null"); - } - - public bool ReadIsBeginArray() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == '[') - { - offset += 1; - return true; - } - else - { - return false; - } - } - - public void ReadIsBeginArrayWithVerify() - { - if (!ReadIsBeginArray()) throw CreateParsingException("["); - } - - public bool ReadIsEndArray() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == ']') - { - offset += 1; - return true; - } - else - { - return false; - } - } - - public void ReadIsEndArrayWithVerify() - { - if (!ReadIsEndArray()) throw CreateParsingException("]"); - } - - public bool ReadIsEndArrayWithSkipValueSeparator(ref int count) - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == ']') - { - offset += 1; - return true; - } - else - { - if (count++ != 0) - { - ReadIsValueSeparatorWithVerify(); - } - return false; - } - } - - /// - /// Convinient pattern of ReadIsBeginArrayWithVerify + while(!ReadIsEndArrayWithSkipValueSeparator) - /// - public bool ReadIsInArray(ref int count) - { - if (count == 0) - { - ReadIsBeginArrayWithVerify(); - if (ReadIsEndArray()) - { - return false; - } - } - else - { - if (ReadIsEndArray()) - { - return false; - } - else - { - ReadIsValueSeparatorWithVerify(); - } - } - - count++; - return true; - } - - public bool ReadIsBeginObject() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == '{') - { - offset += 1; - return true; - } - else - { - return false; - } - } - - public void ReadIsBeginObjectWithVerify() - { - if (!ReadIsBeginObject()) throw CreateParsingException("{"); - } - - public bool ReadIsEndObject() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == '}') - { - offset += 1; - return true; - } - else - { - return false; - } - } - public void ReadIsEndObjectWithVerify() - { - if (!ReadIsEndObject()) throw CreateParsingException("}"); - } - - public bool ReadIsEndObjectWithSkipValueSeparator(ref int count) - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == '}') - { - offset += 1; - return true; - } - else - { - if (count++ != 0) - { - ReadIsValueSeparatorWithVerify(); - } - return false; - } - } - - /// - /// Convinient pattern of ReadIsBeginObjectWithVerify + while(!ReadIsEndObjectWithSkipValueSeparator) - /// - public bool ReadIsInObject(ref int count) - { - if (count == 0) - { - ReadIsBeginObjectWithVerify(); - if (ReadIsEndObject()) - { - return false; - } - } - else - { - if (ReadIsEndObject()) - { - return false; - } - else - { - ReadIsValueSeparatorWithVerify(); - } - } - - count++; - return true; - } - - public bool ReadIsValueSeparator() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == ',') - { - offset += 1; - return true; - } - else - { - return false; - } - } - - public void ReadIsValueSeparatorWithVerify() - { - if (!ReadIsValueSeparator()) throw CreateParsingException(","); - } - - public bool ReadIsNameSeparator() - { - SkipWhiteSpace(); - if (IsInRange && bytes[offset] == ':') - { - offset += 1; - return true; - } - else - { - return false; - } - } - - public void ReadIsNameSeparatorWithVerify() - { - if (!ReadIsNameSeparator()) throw CreateParsingException(":"); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static int GetCodePoint(char a, char b, char c, char d) - { - return (((((ToNumber(a) * 16) + ToNumber(b)) * 16) + ToNumber(c)) * 16) + ToNumber(d); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static int ToNumber(char x) - { - if ('0' <= x && x <= '9') - { - return x - '0'; - } - else if ('a' <= x && x <= 'f') - { - return x - 'a' + 10; - } - else if ('A' <= x && x <= 'F') - { - return x - 'A' + 10; - } - throw new JsonParsingException("Invalid Character" + x); - } - - /// Get raw string-span(do not unescape) - public ReadOnlySpan ReadStringSegmentRaw() - { - ReadOnlySpan key = default(ReadOnlySpan); - if (ReadIsNull()) - { - key = nullTokenSegment; - } - else - { - // SkipWhiteSpace is already called from IsNull - if (bytes[offset++] != '\"') throw CreateParsingException("\""); - - var from = offset; - - for (int i = offset; i < bytes.Length; i++) - { - if (bytes[i] == (char)'\"') - { - // is escape? - if (bytes[i - 1] == (char)'\\') - { - continue; - } - else - { - offset = i + 1; - goto OK; - } - } - } - throw CreateParsingExceptionMessage("not found end string."); - - OK: - key = bytes.Slice(from, offset - from - 1); - } - - return key; - } - - /// Get raw string-span(do not unescape) + ReadIsNameSeparatorWithVerify - public ReadOnlySpan ReadPropertyNameSegmentRaw() - { - var key = ReadStringSegmentRaw(); - ReadIsNameSeparatorWithVerify(); - return key; - } - - static bool IsWordBreak(byte c) - { - switch (c) - { - case (byte)' ': - case (byte)'{': - case (byte)'}': - case (byte)'[': - case (byte)']': - case (byte)',': - case (byte)':': - case (byte)'\"': - return true; - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - case 20: - case 21: - case 22: - case 23: - case 24: - case 25: - case 26: - case 27: - case 28: - case 29: - case 30: - case 31: - case 33: - case 35: - case 36: - case 37: - case 38: - case 39: - case 40: - case 41: - case 42: - case 43: - case 45: - case 46: - case 47: - case 48: - case 49: - case 50: - case 51: - case 52: - case 53: - case 54: - case 55: - case 56: - case 57: - case 59: - case 60: - case 61: - case 62: - case 63: - case 64: - case 65: - case 66: - case 67: - case 68: - case 69: - case 70: - case 71: - case 72: - case 73: - case 74: - case 75: - case 76: - case 77: - case 78: - case 79: - case 80: - case 81: - case 82: - case 83: - case 84: - case 85: - case 86: - case 87: - case 88: - case 89: - case 90: - case 92: - case 94: - case 95: - case 96: - case 97: - case 98: - case 99: - case 100: - case 101: - case 102: - case 103: - case 104: - case 105: - case 106: - case 107: - case 108: - case 109: - case 110: - case 111: - case 112: - case 113: - case 114: - case 115: - case 116: - case 117: - case 118: - case 119: - case 120: - case 121: - case 122: - default: - return false; - } - } - - public void ReadNext() - { - var token = GetCurrentJsonToken(); - ReadNextCore(token); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - void ReadNextCore(JsonToken token) - { - switch (token) - { - case JsonToken.BeginObject: - case JsonToken.BeginArray: - case JsonToken.ValueSeparator: - case JsonToken.NameSeparator: - case JsonToken.EndObject: - case JsonToken.EndArray: - offset += 1; - break; - case JsonToken.True: - case JsonToken.Null: - offset += 4; - break; - case JsonToken.False: - offset += 5; - break; - case JsonToken.String: - offset += 1; // position is "\""; - for (int i = offset; i < bytes.Length; i++) - { - if (bytes[i] == (char)'\"') - { - // is escape? - if (bytes[i - 1] == (char)'\\') - { - continue; - } - else - { - offset = i + 1; - return; // end - } - } - } - throw CreateParsingExceptionMessage("not found end string."); - case JsonToken.Number: - for (int i = offset; i < bytes.Length; i++) - { - if (IsWordBreak(bytes[i])) - { - offset = i; - return; - } - } - offset = bytes.Length; - break; - case JsonToken.None: - default: - break; - } - } - - public void ReadNextBlock() - { - var stack = 0; - - AGAIN: - var token = GetCurrentJsonToken(); - switch (token) - { - case JsonToken.BeginObject: - case JsonToken.BeginArray: - offset++; - stack++; - goto AGAIN; - case JsonToken.EndObject: - case JsonToken.EndArray: - offset++; - stack--; - if (stack != 0) - { - goto AGAIN; - } - break; - case JsonToken.True: - case JsonToken.False: - case JsonToken.Null: - case JsonToken.String: - case JsonToken.Number: - case JsonToken.NameSeparator: - case JsonToken.ValueSeparator: - do - { - ReadNextCore(token); - token = GetCurrentJsonToken(); - } while (stack != 0 && !((int)token < 5)); // !(None, Begin/EndObject, Begin/EndArray) - - if (stack != 0) - { - goto AGAIN; - } - break; - case JsonToken.None: - default: - break; - } - } - - public int ReadInt32() - { - return checked((int)ReadInt64()); - } - - public long ReadInt64() - { - SkipWhiteSpace(); - - int readCount; - var v = NumberConverter.ReadInt64(bytes, offset, out readCount); - if (readCount == 0) - { - throw CreateParsingException("Number Token"); - } - - offset += readCount; - return v; - } - - public ReadOnlySpan ReadNumberSegment() - { - SkipWhiteSpace(); - var initialOffset = offset; - for (int i = offset; i < bytes.Length; i++) - { - if (!NumberConverter.IsNumberRepresentation(bytes[i])) - { - offset = i; - goto END; - } - } - offset = bytes.Length; - - END: - return bytes.Slice(initialOffset, offset - initialOffset); - } - - // return last offset. - static int ReadComment(ReadOnlySpan bytes, int offset) - { - // current token is '/' - if (bytes[offset + 1] == '/') - { - // single line - offset += 2; - for (int i = offset; i < bytes.Length; i++) - { - if (bytes[i] == '\r' || bytes[i] == '\n') - { - return i; - } - } - - throw new JsonParsingException("Can not find end token of single line comment(\r or \n)."); - } - else if (bytes[offset + 1] == '*') - { - - offset += 2; // '/' + '*'; - for (int i = offset; i < bytes.Length; i++) - { - if (bytes[i] == '*' && bytes[i + 1] == '/') - { - return i + 1; - } - } - throw new JsonParsingException("Can not find end token of multi line comment(*/)."); - } - - return offset; - } - } - - internal class JsonParsingException : Exception - { - int limit; - public int Offset { get; private set; } - public string ActualChar { get; set; } - - public JsonParsingException(string message) - : base(message) - { - - } - - public JsonParsingException(string message, int offset, int limit, string actualChar) - : base(message) - { - this.Offset = offset; - this.ActualChar = actualChar; - this.limit = limit; - } - } -} diff --git a/src/LitJWT/JsonParsingException.cs b/src/LitJWT/JsonParsingException.cs new file mode 100644 index 0000000..ae51205 --- /dev/null +++ b/src/LitJWT/JsonParsingException.cs @@ -0,0 +1,28 @@ +//using System; + +//// port from Utf8Json + +//namespace LitJWT + +//{ +// internal class JsonParsingException : Exception +// { +// int limit; +// public int Offset { get; private set; } +// public string ActualChar { get; set; } + +// public JsonParsingException(string message) +// : base(message) +// { + +// } + +// public JsonParsingException(string message, int offset, int limit, string actualChar) +// : base(message) +// { +// this.Offset = offset; +// this.ActualChar = actualChar; +// this.limit = limit; +// } +// } +//} diff --git a/src/LitJWT/JwtDecoder.cs b/src/LitJWT/JwtDecoder.cs index b7ff1e1..e50700f 100644 --- a/src/LitJWT/JwtDecoder.cs +++ b/src/LitJWT/JwtDecoder.cs @@ -1,11 +1,12 @@ using System; using System.Buffers; using System.Text; -using Utf8Json; +using System.Text.Json; namespace LitJWT { public delegate T PayloadParser(ReadOnlySpan payload); + internal delegate T InternalPayloadParser(ReadOnlySpan payload, JsonSerializerOptions serializerOptions); public enum DecodeResult { @@ -24,15 +25,27 @@ public enum DecodeResult public class JwtDecoder { readonly JwtAlgorithmResolver resolver; + readonly JsonSerializerOptions serializerOptions; public JwtDecoder(params IJwtAlgorithm[] algorithms) : this(new JwtAlgorithmResolver(algorithms)) { } + public JwtDecoder(IJwtAlgorithm[] algorithms, JsonSerializerOptions serializerOptions) + : this(new JwtAlgorithmResolver(algorithms), serializerOptions) + { + } + public JwtDecoder(JwtAlgorithmResolver resolver) + : this(resolver, null) + { + } + + public JwtDecoder(JwtAlgorithmResolver resolver, JsonSerializerOptions serializerOptions) { this.resolver = resolver; + this.serializerOptions = serializerOptions; } static void Split(ReadOnlySpan text, out ReadOnlySpan header, out ReadOnlySpan payload, out ReadOnlySpan headerAndPayload, out ReadOnlySpan signature) @@ -136,6 +149,10 @@ public string GetPayloadJson(ReadOnlySpan utf8token) } } + public DecodeResult TryDecode(ReadOnlySpan utf8token, out T payloadResult) => TryDecodeCore(utf8token, static (x, options) => JsonSerializer.Deserialize(x, options), out payloadResult); + public DecodeResult TryDecode(string token, out T payloadResult) => TryDecodeCore(token.AsSpan(), static (x, options) => JsonSerializer.Deserialize(x, options), out payloadResult); + public DecodeResult TryDecode(ReadOnlySpan token, out T payloadResult) => TryDecodeCore(token, static (x, options) => JsonSerializer.Deserialize(x, options), out payloadResult); + public DecodeResult TryDecode(ReadOnlySpan utf8token, PayloadParser payloadParser, out T payloadResult) { Split(utf8token, out var header, out var payload, out var headerAndPayload, out var signature); @@ -155,18 +172,16 @@ public DecodeResult TryDecode(ReadOnlySpan utf8token, PayloadParser return DecodeResult.InvalidBase64UrlHeader; } - var reader = new JsonReader(bytes.Slice(0, bytesWritten)); - var count = 0; - while (reader.ReadIsInObject(ref count)) + var reader = new System.Text.Json.Utf8JsonReader(bytes.Slice(0, bytesWritten)); + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) break; + // try to read algorithm span. - if (reader.ReadPropertyNameSegmentRaw().SequenceEqual(JwtConstantsUtf8.Algorithm)) + if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(JwtConstantsUtf8.Algorithm)) { - algorithm = resolver.Resolve(reader.ReadStringSegmentRaw()); - } - else - { - reader.ReadNextBlock(); + reader.Read(); + algorithm = resolver.Resolve(reader.ValueSpan); } } } @@ -187,24 +202,23 @@ public DecodeResult TryDecode(ReadOnlySpan utf8token, PayloadParser } var decodedPayload = bytes.Slice(0, bytesWritten); - - var reader = new JsonReader(decodedPayload); - var count = 0; - while (reader.ReadIsInObject(ref count)) + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) { - // try to read algorithm span. - var rawSegment = reader.ReadPropertyNameSegmentRaw(); - if (rawSegment.SequenceEqual(JwtConstantsUtf8.Expiration)) - { - expiry = reader.ReadInt64(); - } - else if (rawSegment.SequenceEqual(JwtConstantsUtf8.NotBefore)) - { - notBefore = reader.ReadInt64(); - } - else + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + if (reader.TokenType == JsonTokenType.PropertyName) { - reader.ReadNextBlock(); + if (reader.ValueTextEquals(JwtConstantsUtf8.Expiration)) + { + reader.Read(); + expiry = reader.GetInt64(); + } + else if (reader.ValueTextEquals(JwtConstantsUtf8.NotBefore)) + { + reader.Read(); + notBefore = reader.GetInt64(); + } } } @@ -277,27 +291,166 @@ public DecodeResult TryDecode(ReadOnlySpan token, PayloadParser payl return DecodeResult.InvalidBase64UrlHeader; } - try + var decodedPayload = bytes.Slice(0, bytesWritten); + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) { - var reader = new JsonReader(bytes.Slice(0, bytesWritten)); - var count = 0; - while (reader.ReadIsInObject(ref count)) + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + // try to read algorithm span. + if (reader.TokenType == JsonTokenType.PropertyName) { - // try to read algorithm span. - if (reader.ReadPropertyNameSegmentRaw().SequenceEqual(JwtConstantsUtf8.Algorithm)) + if (reader.ValueTextEquals(JwtConstantsUtf8.Algorithm)) { - algorithm = resolver.Resolve(reader.ReadStringSegmentRaw()); + if (!reader.Read()) + { + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; + } + algorithm = resolver.Resolve(reader.ValueSpan); } - else + } + } + } + + // parsing payload. + long? expiry = null; + long? notBefore = null; + { + var rentBytes = ArrayPool.Shared.Rent(Base64.GetMaxBase64UrlDecodeLength(payload.Length)); + try + { + Span bytes = rentBytes.AsSpan(); + if (!Base64.TryFromBase64UrlChars(payload, bytes, out var bytesWritten)) + { + payloadResult = default; + return DecodeResult.InvalidBase64UrlPayload; + } + + var decodedPayload = bytes.Slice(0, bytesWritten); + + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) + { + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + if (reader.TokenType == JsonTokenType.PropertyName) { - reader.ReadNextBlock(); + if (reader.ValueTextEquals(JwtConstantsUtf8.Expiration)) + { + if (!reader.Read()) + { + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; + } + expiry = reader.GetInt64(); + } + else if (reader.ValueTextEquals(JwtConstantsUtf8.NotBefore)) + { + if (!reader.Read()) + { + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; + } + notBefore = reader.GetInt64(); + } } } + + // and custom deserialize. + payloadResult = payloadParser(decodedPayload); } - catch (JsonParsingException) + finally { - payloadResult = default; - return DecodeResult.InvalidHeaderFormat; + ArrayPool.Shared.Return(rentBytes); + } + } + if (expiry != null) + { + var now = DateTimeOffset.UtcNow; + var expireTime = DateTimeOffset.FromUnixTimeSeconds(expiry.Value); + if (expireTime - now < TimeSpan.Zero) + { + return DecodeResult.FailedVerifyExpire; + } + } + if (notBefore != null) + { + var now = DateTimeOffset.UtcNow; + var notBeforeTime = DateTimeOffset.FromUnixTimeSeconds(notBefore.Value); + if (now - notBeforeTime < TimeSpan.Zero) + { + return DecodeResult.FailedVerifyNotBefore; + } + } + + // parsing signature. + { + if (algorithm == null) + { + return DecodeResult.AlgorithmNotExists; + } + + var rentBuffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(headerAndPayload.Length)); + try + { + Span signatureDecoded = stackalloc byte[Base64.GetMaxBase64UrlDecodeLength(signature.Length)]; + if (!Base64.TryFromBase64UrlChars(signature, signatureDecoded, out var bytesWritten)) + { + return DecodeResult.InvalidBase64UrlSignature; + } + signatureDecoded = signatureDecoded.Slice(0, bytesWritten); + + var signBuffer = rentBuffer.AsSpan(); + var byteCount = Encoding.UTF8.GetBytes(headerAndPayload, signBuffer); + signBuffer = signBuffer.Slice(0, byteCount); + if (!algorithm.Verify(signBuffer, signatureDecoded)) + { + return DecodeResult.FailedVerifySignature; + } + } + finally + { + ArrayPool.Shared.Return(rentBuffer); + } + } + + // all ok + return DecodeResult.Success; + } + + // note:ugly copy and paste code... + DecodeResult TryDecodeCore(ReadOnlySpan utf8token, InternalPayloadParser payloadParser, out T payloadResult) + { + Split(utf8token, out var header, out var payload, out var headerAndPayload, out var signature); + + IJwtAlgorithm algorithm = null; + + // parsing header. + { + // first, try quick match + algorithm = resolver.ResolveFromBase64Header(header); + if (algorithm == null) + { + Span bytes = stackalloc byte[Base64.GetMaxBase64UrlDecodeLength(header.Length)]; + if (!Base64.TryFromBase64UrlUtf8(header, bytes, out var bytesWritten)) + { + payloadResult = default; + return DecodeResult.InvalidBase64UrlHeader; + } + + var reader = new System.Text.Json.Utf8JsonReader(bytes.Slice(0, bytesWritten)); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) break; + + // try to read algorithm span. + if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals(JwtConstantsUtf8.Algorithm)) + { + reader.Read(); + algorithm = resolver.Resolve(reader.ValueSpan); + } + } } } @@ -309,44 +462,165 @@ public DecodeResult TryDecode(ReadOnlySpan token, PayloadParser payl try { Span bytes = rentBytes.AsSpan(); - if (!Base64.TryFromBase64UrlChars(payload, bytes, out var bytesWritten)) + if (!Base64.TryFromBase64UrlUtf8(payload, bytes, out var bytesWritten)) { payloadResult = default; return DecodeResult.InvalidBase64UrlPayload; } var decodedPayload = bytes.Slice(0, bytesWritten); - - try + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) { - var reader = new JsonReader(decodedPayload); - var count = 0; - while (reader.ReadIsInObject(ref count)) + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + if (reader.TokenType == JsonTokenType.PropertyName) { - // try to read algorithm span. - var rawSegment = reader.ReadPropertyNameSegmentRaw(); - if (rawSegment.SequenceEqual(JwtConstantsUtf8.Expiration)) + if (reader.ValueTextEquals(JwtConstantsUtf8.Expiration)) { - expiry = reader.ReadInt64(); + reader.Read(); + expiry = reader.GetInt64(); } - else if (rawSegment.SequenceEqual(JwtConstantsUtf8.NotBefore)) + else if (reader.ValueTextEquals(JwtConstantsUtf8.NotBefore)) { - notBefore = reader.ReadInt64(); + reader.Read(); + notBefore = reader.GetInt64(); } - else + } + } + + // and custom deserialize. + payloadResult = payloadParser(decodedPayload, serializerOptions); + } + finally + { + ArrayPool.Shared.Return(rentBytes); + } + } + if (expiry != null) + { + var expireTime = DateTimeOffset.FromUnixTimeSeconds(expiry.Value); + if (expireTime - DateTimeOffset.UtcNow < TimeSpan.Zero) + { + return DecodeResult.FailedVerifyExpire; + } + } + if (notBefore != null) + { + var notBeforeTime = DateTimeOffset.FromUnixTimeSeconds(notBefore.Value); + if (DateTimeOffset.UtcNow - notBeforeTime < TimeSpan.Zero) + { + return DecodeResult.FailedVerifyNotBefore; + } + } + + // parsing signature. + { + if (algorithm == null) + { + return DecodeResult.AlgorithmNotExists; + } + + Span signatureDecoded = stackalloc byte[Base64.GetMaxBase64UrlDecodeLength(signature.Length)]; + if (!Base64.TryFromBase64UrlUtf8(signature, signatureDecoded, out var bytesWritten)) + { + return DecodeResult.InvalidBase64UrlSignature; + } + signatureDecoded = signatureDecoded.Slice(0, bytesWritten); + + if (!algorithm.Verify(headerAndPayload, signatureDecoded)) + { + return DecodeResult.FailedVerifySignature; + } + } + + // all ok + return DecodeResult.Success; + } + + DecodeResult TryDecodeCore(ReadOnlySpan token, InternalPayloadParser payloadParser, out T payloadResult) + { + Split(token, out var header, out var payload, out var headerAndPayload, out var signature); + + IJwtAlgorithm algorithm = null; + + // parsing header. + { + Span bytes = stackalloc byte[Base64.GetMaxBase64UrlDecodeLength(header.Length)]; + if (!Base64.TryFromBase64UrlChars(header, bytes, out var bytesWritten)) + { + payloadResult = default; + return DecodeResult.InvalidBase64UrlHeader; + } + + var decodedPayload = bytes.Slice(0, bytesWritten); + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) + { + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + // try to read algorithm span. + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals(JwtConstantsUtf8.Algorithm)) + { + if (!reader.Read()) { - reader.ReadNextBlock(); + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; } + algorithm = resolver.Resolve(reader.ValueSpan); } - } - catch (JsonParsingException) + } + } + + // parsing payload. + long? expiry = null; + long? notBefore = null; + { + var rentBytes = ArrayPool.Shared.Rent(Base64.GetMaxBase64UrlDecodeLength(payload.Length)); + try + { + Span bytes = rentBytes.AsSpan(); + if (!Base64.TryFromBase64UrlChars(payload, bytes, out var bytesWritten)) { payloadResult = default; - return DecodeResult.InvalidPayloadFormat; + return DecodeResult.InvalidBase64UrlPayload; } + + var decodedPayload = bytes.Slice(0, bytesWritten); + + var reader = new System.Text.Json.Utf8JsonReader(decodedPayload); + while (reader.Read()) + { + if (reader.TokenType == System.Text.Json.JsonTokenType.EndObject) break; + + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals(JwtConstantsUtf8.Expiration)) + { + if (!reader.Read()) + { + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; + } + expiry = reader.GetInt64(); + } + else if (reader.ValueTextEquals(JwtConstantsUtf8.NotBefore)) + { + if (!reader.Read()) + { + payloadResult = default; + return DecodeResult.InvalidHeaderFormat; + } + notBefore = reader.GetInt64(); + } + } + } + // and custom deserialize. - payloadResult = payloadParser(decodedPayload); + payloadResult = payloadParser(decodedPayload, serializerOptions); } finally { diff --git a/src/LitJWT/JwtEncoder.cs b/src/LitJWT/JwtEncoder.cs index 5ef2801..fc9f570 100644 --- a/src/LitJWT/JwtEncoder.cs +++ b/src/LitJWT/JwtEncoder.cs @@ -1,17 +1,20 @@ using System; using System.Buffers; +using System.Text.Json; namespace LitJWT { public class JwtEncoder { - IJwtAlgorithm signAlgorithm; + readonly IJwtAlgorithm signAlgorithm; + readonly JsonSerializerOptions serializerOptions; [ThreadStatic] static Utf8BufferWriter encodeWriter = null; public IJwtAlgorithm SignAlgorithm => signAlgorithm; + static Utf8BufferWriter GetWriter() { if (encodeWriter == null) @@ -24,8 +27,22 @@ static Utf8BufferWriter GetWriter() public JwtEncoder(IJwtAlgorithm signAlgorithm) { this.signAlgorithm = signAlgorithm; + this.serializerOptions = null; + } + + public JwtEncoder(IJwtAlgorithm signAlgorithm, JsonSerializerOptions serializerOptions) + { + this.signAlgorithm = signAlgorithm; + this.serializerOptions = serializerOptions; } + public string Encode(T payload, TimeSpan expire) => Encode(payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public string Encode(T payload, DateTimeOffset? expire) => Encode(payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public byte[] EncodeAsUtf8Bytes(T payload, TimeSpan expire) => EncodeAsUtf8Bytes(payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public byte[] EncodeAsUtf8Bytes(T payload, DateTimeOffset? expire) => EncodeAsUtf8Bytes(payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public void Encode(IBufferWriter bufferWriter, T payload, TimeSpan expire) => Encode(bufferWriter, payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public void Encode(IBufferWriter bufferWriter, T payload, DateTimeOffset? expire) => Encode(bufferWriter, payload, expire, static (x, writer) => writer.Write(JsonSerializer.SerializeToUtf8Bytes(x, writer.serializerOptions))); + public string Encode(T payload, TimeSpan expire, Action payloadWriter) { return Encode(payload, DateTimeOffset.UtcNow.Add(expire), payloadWriter); @@ -36,7 +53,7 @@ public string Encode(T payload, DateTimeOffset? expire, Action var buffer = GetWriter(); try { - var writer = new JwtWriter(buffer, signAlgorithm, expire); + var writer = new JwtWriter(buffer, signAlgorithm, expire, serializerOptions); payloadWriter(payload, writer); return buffer.ToString(); } @@ -56,7 +73,7 @@ public byte[] EncodeAsUtf8Bytes(T payload, DateTimeOffset? expire, Action(IBufferWriter bufferWriter, T payload, TimeSpan expi public void Encode(IBufferWriter bufferWriter, T payload, DateTimeOffset? expire, Action payloadWriter) { - var writer = new JwtWriter(bufferWriter, signAlgorithm, expire); + var writer = new JwtWriter(bufferWriter, signAlgorithm, expire, serializerOptions); payloadWriter(payload, writer); } } diff --git a/src/LitJWT/JwtWriter.cs b/src/LitJWT/JwtWriter.cs index 0c12d9c..760ea0e 100644 --- a/src/LitJWT/JwtWriter.cs +++ b/src/LitJWT/JwtWriter.cs @@ -1,7 +1,7 @@ using System; using System.Buffers; using System.Text; -using Utf8Json.Internal; +using System.Text.Json; namespace LitJWT { @@ -15,7 +15,14 @@ public readonly struct JwtWriter readonly IJwtAlgorithm algorithm; readonly long? expire; + internal readonly JsonSerializerOptions serializerOptions; + public JwtWriter(IBufferWriter writer, IJwtAlgorithm algorithm, DateTimeOffset? expire) + : this(writer, algorithm, expire, null) + { + } + + internal JwtWriter(IBufferWriter writer, IJwtAlgorithm algorithm, DateTimeOffset? expire, JsonSerializerOptions serializerOptions) { this.writer = writer; this.algorithm = algorithm; @@ -27,6 +34,7 @@ public JwtWriter(IBufferWriter writer, IJwtAlgorithm algorithm, DateTimeOf { this.expire = null; } + this.serializerOptions = serializerOptions; } public void Write(ReadOnlySpan payload) diff --git a/src/LitJWT/LitJWT.csproj b/src/LitJWT/LitJWT.csproj index c29e5e3..e4d8e8d 100644 --- a/src/LitJWT/LitJWT.csproj +++ b/src/LitJWT/LitJWT.csproj @@ -1,29 +1,31 @@  - - netcoreapp2.1;netstandard2.1 - 7.3 - Library - true - opensource.snk - true - true - True - 1701;1702;1705;1591 - Cysharp + + netstandard2.1;net5.0;net6.0 + 9.0 + Library + true + opensource.snk + true + true + True + 1701;1702;1705;1591 + Cysharp - - LitJWT - $(Version) - Cysharp - Cysharp - Lightweight, Fast JWT(JSON Web Token) implementation for .NET Core. - https://github.com/Cysharp/LitJWT - $(PackageProjectUrl) - git - jwt, auth + + LitJWT + $(Version) + Cysharp + Cysharp + Lightweight, Fast JWT(JSON Web Token) implementation for .NET Core. + https://github.com/Cysharp/LitJWT + $(PackageProjectUrl) + git + jwt, auth + - - + + + diff --git a/src/LitJWT/Json/NumberConverter.cs b/src/LitJWT/NumberConverter.cs similarity index 99% rename from src/LitJWT/Json/NumberConverter.cs rename to src/LitJWT/NumberConverter.cs index cb54426..aba6025 100644 --- a/src/LitJWT/Json/NumberConverter.cs +++ b/src/LitJWT/NumberConverter.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.CompilerServices; -namespace Utf8Json.Internal +namespace LitJWT { /// /// zero-allocate itoa, dtoa, atoi, atod converters. diff --git a/tests/LitJWT.Tests/JsonBugTest.cs b/tests/LitJWT.Tests/JsonBugTest.cs new file mode 100644 index 0000000..4221f05 --- /dev/null +++ b/tests/LitJWT.Tests/JsonBugTest.cs @@ -0,0 +1,27 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace LitJWT.Tests +{ + public class JsonBugTest + { + [Fact] + public void Test() + { + var key = new byte[] { 1, 2, 3 }; + var encoder = new LitJWT.JwtEncoder(new LitJWT.Algorithms.HS256Algorithm(key)); + var decoder = new LitJWT.JwtDecoder(encoder.SignAlgorithm); + var bytes = encoder.EncodeAsUtf8Bytes(new User { Name = "foo\\" }, System.TimeSpan.FromMinutes(5), (x, writer) => writer.Write(System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(x))); + var result = decoder.TryDecode(bytes, x => System.Text.Json.JsonSerializer.Deserialize(x), out var session); + session.Name.Should().Be("foo\\"); + } + } + + public class User + { + public string Name { get; set; } + } +}