From 1e57785a8e381a5649733be9da08249df190cf31 Mon Sep 17 00:00:00 2001 From: Ziya Suzen Date: Fri, 19 Jul 2024 22:32:36 +0100 Subject: [PATCH] Initial commit --- .editorconfig | 190 +++++++ .gitattributes | 15 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/defect.yml | 44 ++ .github/ISSUE_TEMPLATE/proposal.yml | 28 + .github/workflows/format.yml | 31 ++ .github/workflows/release.yml | 58 +++ .github/workflows/test.yml | 138 +++++ .gitignore | 484 ++++++++++++++++++ Default.runsettings | 14 + Directory.Build.props | 43 ++ Icon.png | Bin 0 -> 3416 bytes LICENSE | 237 +++++++++ .../NATS.Jwt.CheckNativeAot.csproj | 17 + NATS.Jwt.CheckNativeAot/Program.cs | 59 +++ NATS.Jwt.Tests/ConnectTests.cs | 242 +++++++++ NATS.Jwt.Tests/NATS.Jwt.Tests.csproj | 32 ++ NATS.Jwt.Tests/NatsGenericClaimsTests.cs | 108 ++++ NATS.Jwt.Tests/NatsJwtTests.cs | 31 ++ NATS.Jwt.Tests/NatsServerProcess.cs | 289 +++++++++++ NATS.Jwt.sln | 67 +++ NATS.Jwt.sln.DotSettings | 2 + NATS.Jwt.snk | Bin 0 -> 596 bytes NATS.Jwt/Internal/EncodingUtils.cs | 42 ++ NATS.Jwt/Internal/JsonConverters.cs | 52 ++ NATS.Jwt/Internal/NatsBufferWriter.cs | 426 +++++++++++++++ NATS.Jwt/Internal/SHA512256.cs | 187 +++++++ NATS.Jwt/JsonContext.cs | 41 ++ NATS.Jwt/Models/JetStreamLimits.cs | 62 +++ NATS.Jwt/Models/JwtClaimsData.cs | 39 ++ NATS.Jwt/Models/JwtHeader.cs | 12 + NATS.Jwt/Models/NatsAccount.cs | 51 ++ NATS.Jwt/Models/NatsAccountClaims.cs | 11 + NATS.Jwt/Models/NatsActivationClaims.cs | 30 ++ .../Models/NatsAuthorizationRequestClaims.cs | 175 +++++++ .../Models/NatsAuthorizationResponseClaims.cs | 30 ++ NATS.Jwt/Models/NatsExport.cs | 63 +++ NATS.Jwt/Models/NatsExternalAuthorization.cs | 19 + NATS.Jwt/Models/NatsGenericClaims.cs | 13 + NATS.Jwt/Models/NatsGenericFields.cs | 19 + NATS.Jwt/Models/NatsGenericFieldsClaims.cs | 11 + NATS.Jwt/Models/NatsImport.cs | 67 +++ NATS.Jwt/Models/NatsMsgTrace.cs | 26 + NATS.Jwt/Models/NatsOperator.cs | 55 ++ NATS.Jwt/Models/NatsOperatorClaims.cs | 11 + NATS.Jwt/Models/NatsOperatorLimits.cs | 130 +++++ NATS.Jwt/Models/NatsPermission.cs | 15 + NATS.Jwt/Models/NatsPermissions.cs | 18 + NATS.Jwt/Models/NatsResponsePermission.cs | 18 + NATS.Jwt/Models/NatsServiceLatency.cs | 14 + NATS.Jwt/Models/NatsUser.cs | 54 ++ NATS.Jwt/Models/NatsUserClaims.cs | 11 + NATS.Jwt/Models/NatsWeightedMapping.cs | 17 + NATS.Jwt/Models/TimeRange.cs | 15 + NATS.Jwt/NATS.Jwt.csproj | 38 ++ NATS.Jwt/NatsJwt.cs | 228 +++++++++ README.md | 107 ++++ version.txt | 1 + 58 files changed, 4242 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/defect.yml create mode 100644 .github/ISSUE_TEMPLATE/proposal.yml create mode 100644 .github/workflows/format.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 Default.runsettings create mode 100644 Directory.Build.props create mode 100644 Icon.png create mode 100644 LICENSE create mode 100644 NATS.Jwt.CheckNativeAot/NATS.Jwt.CheckNativeAot.csproj create mode 100644 NATS.Jwt.CheckNativeAot/Program.cs create mode 100644 NATS.Jwt.Tests/ConnectTests.cs create mode 100644 NATS.Jwt.Tests/NATS.Jwt.Tests.csproj create mode 100644 NATS.Jwt.Tests/NatsGenericClaimsTests.cs create mode 100644 NATS.Jwt.Tests/NatsJwtTests.cs create mode 100644 NATS.Jwt.Tests/NatsServerProcess.cs create mode 100644 NATS.Jwt.sln create mode 100644 NATS.Jwt.sln.DotSettings create mode 100644 NATS.Jwt.snk create mode 100644 NATS.Jwt/Internal/EncodingUtils.cs create mode 100644 NATS.Jwt/Internal/JsonConverters.cs create mode 100644 NATS.Jwt/Internal/NatsBufferWriter.cs create mode 100644 NATS.Jwt/Internal/SHA512256.cs create mode 100644 NATS.Jwt/JsonContext.cs create mode 100644 NATS.Jwt/Models/JetStreamLimits.cs create mode 100644 NATS.Jwt/Models/JwtClaimsData.cs create mode 100644 NATS.Jwt/Models/JwtHeader.cs create mode 100644 NATS.Jwt/Models/NatsAccount.cs create mode 100644 NATS.Jwt/Models/NatsAccountClaims.cs create mode 100644 NATS.Jwt/Models/NatsActivationClaims.cs create mode 100644 NATS.Jwt/Models/NatsAuthorizationRequestClaims.cs create mode 100644 NATS.Jwt/Models/NatsAuthorizationResponseClaims.cs create mode 100644 NATS.Jwt/Models/NatsExport.cs create mode 100644 NATS.Jwt/Models/NatsExternalAuthorization.cs create mode 100644 NATS.Jwt/Models/NatsGenericClaims.cs create mode 100644 NATS.Jwt/Models/NatsGenericFields.cs create mode 100644 NATS.Jwt/Models/NatsGenericFieldsClaims.cs create mode 100644 NATS.Jwt/Models/NatsImport.cs create mode 100644 NATS.Jwt/Models/NatsMsgTrace.cs create mode 100644 NATS.Jwt/Models/NatsOperator.cs create mode 100644 NATS.Jwt/Models/NatsOperatorClaims.cs create mode 100644 NATS.Jwt/Models/NatsOperatorLimits.cs create mode 100644 NATS.Jwt/Models/NatsPermission.cs create mode 100644 NATS.Jwt/Models/NatsPermissions.cs create mode 100644 NATS.Jwt/Models/NatsResponsePermission.cs create mode 100644 NATS.Jwt/Models/NatsServiceLatency.cs create mode 100644 NATS.Jwt/Models/NatsUser.cs create mode 100644 NATS.Jwt/Models/NatsUserClaims.cs create mode 100644 NATS.Jwt/Models/NatsWeightedMapping.cs create mode 100644 NATS.Jwt/Models/TimeRange.cs create mode 100644 NATS.Jwt/NATS.Jwt.csproj create mode 100644 NATS.Jwt/NatsJwt.cs create mode 100644 README.md create mode 100644 version.txt diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1cf30fa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,190 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# License header +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f2a3d69 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +* text=auto eol=lf +*.csproj text=auto eol=crlf + +*.enc binary +*.eot binary +*.ico binary +*.jpg binary +*.otf binary +*.pem binary +*.pfx binary +*.png binary +*.ttf binary +*.woff binary +*.woff2 binary +*.pfx binary diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ed07af4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Chat + url: https://slack.nats.io + about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! diff --git a/.github/ISSUE_TEMPLATE/defect.yml b/.github/ISSUE_TEMPLATE/defect.yml new file mode 100644 index 0000000..bd3188f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/defect.yml @@ -0,0 +1,44 @@ +--- +name: Defect +description: Report a defect, such as a bug or regression. +labels: + - defect +body: + - type: textarea + id: observed + attributes: + label: Observed behavior + description: Describe the unexpected behavior or performance regression you are observing. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: Describe the expected behavior or performance characteristics. + validations: + required: true + - type: textarea + id: versions + attributes: + label: Library version + description: |- + Provide the version you were using when the detect was observed. + validations: + required: true + - type: textarea + id: environment + attributes: + label: Host environment + description: |- + Specify any relevant details about the host environment the library was running in, + such as operating system, CPU architecture, container runtime, etc. + validations: + required: false + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: Provide as many concrete steps to reproduce the defect. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/proposal.yml b/.github/ISSUE_TEMPLATE/proposal.yml new file mode 100644 index 0000000..7158383 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.yml @@ -0,0 +1,28 @@ +--- +name: Proposal +description: Propose an enhancement or new feature. +labels: + - proposal +body: + - type: textarea + id: change + attributes: + label: Proposed change + description: This could be a behavior change, enhanced API, or a new feature. + validations: + required: true + - type: textarea + id: usecase + attributes: + label: Use case + description: What is the use case or general motivation for this proposal? + validations: + required: true + - type: textarea + id: contribute + attributes: + label: Contribution + description: |- + Are you intending or interested in contributing code for this proposal if accepted? + validations: + required: false diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..5669f3a --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,31 @@ +name: Format + +on: + pull_request: {} + + # to be removed. only needed for the initial setup. running on pull requests is enough. + push: { branches: [ main ] } + +jobs: + check: + name: check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.x' + + - name: Check formatting + run: | + if dotnet format --verify-no-changes; then + echo "formatting passed" + else + rc="$?" + echo "formatting failed; run 'dotnet format'" >&2 + # exit "$rc" + exit 0 # to be removed + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d39e6ed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + nuget: + name: dotnet + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - id: tag + name: Determine tag + run: | + version="$(head -n 1 version.txt)" + ref_name="v$version" + create=true + if [ "$(git ls-remote origin "refs/tags/$ref_name" | wc -l)" = "1" ]; then + create=false + fi + + echo "version=$version" | tee -a "$GITHUB_OUTPUT" + echo "ref-name=$ref_name" | tee -a "$GITHUB_OUTPUT" + echo "create=$create" | tee -a "$GITHUB_OUTPUT" + + - if: ${{ fromJSON(steps.tag.outputs.create) }} + name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.x' + + - if: ${{ fromJSON(steps.tag.outputs.create) }} + name: Pack + # https://learn.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg + # https://devblogs.microsoft.com/dotnet/producing-packages-with-source-link/ + run: dotnet pack -c Release -o dist -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:ContinuousIntegrationBuild=true + + - if: ${{ fromJSON(steps.tag.outputs.create) }} + name: Push + run: | + cd dist + ls -lh + # this should upload snupkgs in the same folder + # TODO: Uncomment the following line + # dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k "${{ secrets.NUGET_API_KEY }}" --skip-duplicate + + - if: ${{ fromJSON(steps.tag.outputs.create) }} + name: Tag + run: | + git tag "${{ steps.tag.outputs.ref-name }}" + git push origin "${{ steps.tag.outputs.ref-name }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ef64c5e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,138 @@ +name: Test + +on: + pull_request: {} + push: + branches: + - main + +jobs: + dotnet: + name: dotnet + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install nats-server + shell: bash + run: | + mkdir tools && cd tools + branch="main" + for i in 1 2 3 + do + curl -sf https://binaries.nats.dev/nats-io/nats-server/v2@$branch | PREFIX=. sh && break || sleep 30 + done + + case "${{ matrix.os }}" in + ubuntu-latest|macos-latest) + sudo mv nats-server /usr/local/bin + ;; + windows-latest) + mv nats-server nats-server.exe + cygpath -w "$(pwd)" | tee -a "$GITHUB_PATH" + ;; + esac + + - name: Check nats-server + run: nats-server -v + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.x + 8.x + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore -p:ContinuousIntegrationBuild=true + + - name: Test + # Collect code coverage + # https://github.com/MarcoRossignoli/coverlet/blob/master/Documentation/KnownIssues.md#tests-fail-if-assembly-is-strong-named + run: dotnet test --no-build --logger:"console;verbosity=normal" --collect:"XPlat Code Coverage" --settings Default.runsettings -- RunConfiguration.DisableAppDomain=true + +# TODO: Enable code coverage upload +# - name: Upload coverage reports to Codecov +# # PRs from external contributors fail: https://github.com/codecov/feedback/issues/301 +# # Only upload coverage reports for PRs from the same repo (not forks) +# if: github.event.pull_request.head.repo.full_name == github.repository || github.ref == 'refs/heads/main' +# uses: codecov/codecov-action@v4.0.1 +# with: +# token: ${{ secrets.CODECOV_TOKEN }} + + - name: Check Native AOT + shell: bash + run: | + echo ">> Set up for AOT compilation..." + export exe_file=NATS.Jwt.CheckNativeAot + export exe_type=ELF + export dotnet_runtime_id=linux-x64 + + echo ">> Checking OS..." + if [ "${{ matrix.os }}" = "windows-latest" ]; then + export exe_file=NATS.Jwt.CheckNativeAot.exe + export exe_type=PE32 + export dotnet_runtime_id=win-x64 + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + export dotnet_runtime_id=osx-x64 + export exe_type=Mach-O + fi + + echo ">> Publishing..." + cd NATS.Jwt.CheckNativeAot + rm -rf bin obj + dotnet publish -r $dotnet_runtime_id -c Release -o dist | tee output.txt + + echo ">> Checking for warnings..." + grep -i warning output.txt && exit 1 + + echo ">> Executable sanity checks..." + cd dist + ls -lh + + echo ">> Executable is of type $exe_type..." + file $exe_file + file $exe_file | grep $exe_type || exit 1 + + echo ">> Executable size checks..." + # Can't be less than a meg and not more than 10 megs. + # Fairly arbitrary, but we want to make sure executable size + # is reasonable so we can be somewhat sure AOT compilation + # happened correctly. + export filesize=0 + + if [ "${{ matrix.os }}" = "windows-latest" ]; then + export filesize=$(stat -c %s $exe_file) + elif [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + export filesize=$(stat -c %s $exe_file) + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + export filesize=$(stat -f %z $exe_file) + fi + + echo ">> File size: $filesize bytes" + if [ $filesize -lt 1048576 ]; then + echo ">> Error: File is less than 1MB." + exit 1 + fi + if [ $filesize -gt 10485760 ]; then + echo ">> Error: File is more than 10MB." + exit 1 + fi + echo ">> File size is within acceptable range." + + echo ">> Running executable..." + ./$exe_file | tee | grep PASS || exit 1 + + echo ">> Run complete." + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104b544 --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/Default.runsettings b/Default.runsettings new file mode 100644 index 0000000..e519501 --- /dev/null +++ b/Default.runsettings @@ -0,0 +1,14 @@ + + + + + + + + + [*Test*]* + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..e6ce6ef --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,43 @@ + + + latest + enable + $(NoWarn);CS1591;SA0001 + true + $(MSBuildThisFileDirectory)NATS.Jwt.snk + false + + + $(NoWarn),CS8618,SA1600,SA1633,SA1101,SA1402,SA1629,SA1623,SA1309,SA1601,SA1633,SA1649,SA1503 + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + $([System.IO.File]::ReadAllText("$(MSBuildThisFileDirectory)version.txt")) + $(Version) + true + CNCF + The NATS Authors + Copyright © The NATS Authors 2016-$([System.DateTime]::Now.ToString(yyyy)) + https://github.com/nats-io/jwt.net + $(PackageProjectUrl) + git + Apache-2.0 + Icon.png + README.md + + + + + + + + + diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a248818bd0b0c3285f7cfb78efb5aeae95e4067 GIT binary patch literal 3416 zcma)9c{J4T_kYiZF&J6OP8dR2v!yVWjD74&)-096gv!#?m|>8zeMyMySqm8nX+#<& zThS-$SYk3UG|@=%^ZEaGzUTYLz4zREpYuBBo^xN%^Lo;-4p#iU61)Ha@LOXnoDU-5 zU*q9A@V&-??+*ef+S$qscrqmQ?I3|(K-;4Mpg9xqClGd!!*Lj|XaG=`|JOiu3o|(Y zz_(^?fp(4cTPsH3T*t);`@55!nTkcqNLWuc@lNnb7kLd~o>W$wCpvYa`Hp+fBoS>Z zZZ(!ckF~4ppB3>-~v!=*b+ttH)qyxw+?;6#0;g}XqdcGWrt;39Tisv=CL52 z6kbyUSKxISSWrNefU5zL%wcj;|GlMX56l3mm(Dta_gF9PpGbC!5mmGo$JKP`9eb*! zav99uns{0Y@?v(0bL~_e+EtgDA_F<&>ciJxXbRbTzgYA4g&fehkvP2@W929QG^H5{c+IW{MJ%dm;#sQj#=|@F(u%R~88I z4at1kX+I0_MFA6b1El2qP*X$>;CR+)=w3aPqw%Wv0mN1**sQ3s;2Al$`)U>NK-43& zXA$;gB#oS^8pg}uDZAn902rAxAv2M$U6Kt2-TAX>p)Yfyz;x1?+rPWs>m6DbFA^Ab z9MFYPuereE|Lx5Fne1?Ae_B1z<}f{-{xM*!^(Y~;-U55s6vZLueuvO2Qbo6px_}3& z{igX9_BO&c1vfVNl4_v)3L!>`FZ=}B9`f}1P8F5frbQa>**y_oCfyM?KWZCXCOmG9 z!UTde<1oZJ7pG(Rr=xA+-9(>5diVK~5fE@pB`3SDN3IG+y?0ySck6oiefoCzy^Rph zHFZputH?`RjS2&FBvdCsZ<{vkMrUxJzkVzer9ep( zMdtp!MJSbSPrATo`1AV_Rv1}-b?tJ8heHW6{m0Xk>nc*e;*FK(F$ZIUvqPxq7O}yO zh_2Z7XPs28cip=XPjb0S2C=U59I*tvAtiPi$(5om40q)&ec#@^%Al-ACTEy!@MJBE z(l<%c8$Ta!Q=rHte{MD98G%E(Tw3;4ji3~wG_KF++g;Yp`|0yf!YagfNwIgBNv5gn36!LX&vgcC^C6e`I6G3|;hNhwDUQf^ zF-GHhBH8~FI*IX{{0?u`kfQl*vyYeMiI>#i(hG|^UM7PzQ0?v8RCwU=Zl#}c)F%Jw z=gUk-k}ST38@%35A%}QsUOv5BZ~3{vYX3W(_Sx`4YQw@x}Zi$z4gSt7b(Cekod}*!|@jn}Ubq+yQ(fkhxX3M|t9E z+L4&s%MHJWWEsdJNx^@%&Y6&{O7IAg@sdUE#i$ESY~_aYmVOre!4E8p9q)(SV{@g* z&+3C_vU`598fRmPD|}opYcHHBerA-&2@3M)vyHsQFBe6;6`SxjFqTpKK`?E=y(&#h z)d0Ti+s$QREL^TC5nqn4+wn5AVjNq5J=fy=eK{`a_;brg%81uM_6vr%y7!i@{Av~& zUc7B5=KDYkWb>4x5S(hB4K2R7FTT&^AnKzAC>b$G_(!)ui1%|z{f86I^28--VNcCl z(zg8Xp5>9w$fu5j>D}J$WTb%X<)nm?nk3=JlMOk2v>zFosCGI0X$ujb$I&Pp<==JJ zoZ;rbxH$K$MQAlc-wTzNy*!W%^XLL9bP(ClM)!%hc0YhmrY)p0uB=u!cwNA_&gfE} zA~}DU5a)3kxfo?5_ZG~s1lZ{VDq0L;5 zkp#5B0UPU7mawV2uIHamhiiT}P26vMqb+sw_Vj-=H;FqKqF%HRO+^p|aM{v@-((l!ISC(eNYRU_NSXNpsP>sqQ0k zyeXne$rV7Hu96gN3D~dYZ>oxbP2Qf07P6@u7IFr~6Hqj=W8p0E)h&0fI}yWUY*#Yx z2*Q)7aKf#rxvf*56G(@u zXJDdqDY}ZU$s@SGRYp&TInpe;eOC1<(v`p_pf)zeM~)Vg?@U_aT9{dG3j#I39$-uV zIyPwdm!D#yyUP`k$UEZL1dW{(L-MPc_<|ojcRhz1Qo01zj7^Sss2DZ3be=%{Z6~fL zeM=;)Xtw#scCR+)LTJuyPeTs?GHJrggMmjRlqRK} zs`wxHIecv?l3{5dkWmnlSND*k9o7Yc85_S6antwMP!v{CHS5vkZY3^2q`GC09Gr&n zWQtMR^$N+3dml%E?vpa|zN-&gYNxnY^hq%@1dLsp+6KNDFdj=e2k}b(N2V}h7=B_! zAq@}QY|)bRsJz0QLvZATX8ot6IXqtjGz~J%tiApxVe&7M{HND>!q)N#pP1Ymi}tWdbVVrx z2ucB@-V&FZXLo{ZfCQ9^|T$q&?T= z(>2OgmL4cInr{tc?Ek+CBS{0rJ69_vngudu~XfhnO5uZoz+|g_noq z3DV4SJiS+R(V{T(6ts|Gz6S!Mi=)$wF`razRpgfWG24!?iQGEhtzKd(~vxhe#1aZocLg}l*y z+bf;@&DnD%zhtesWj*a0%A~ngx8TT$N4n&RYSaL7qdt_6HD;ju-p9 zRGhAU^0SIf-Nb?a-p^t4$VHm=}KrBn6 zs)E9D-QPMJmw1MlqF9j7QnNUVm3f`^rnzYwB_X*)RvdK_!h){Qn+sU7a0=}WBh3DK ysL>e>g2jFRPPW+GdS|WMVn>5Wa?UF7hucw8`J+H3==Z^I2(Y$vuy|q?K>R;pEF>NP literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4b5569 --- /dev/null +++ b/LICENSE @@ -0,0 +1,237 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +SUBCOMPONENTS: + +This project includes subcomponents with separate copyright notices and +license terms. Your use of the source code for the these subcomponents +is subject to the terms and conditions of the following licenses. + +-------------------------------------------------------------------------------- +For components from the Cysharp/AlterNats project, +which can be found at https://github.com/Cysharp/AlterNats: + +MIT License + +Copyright (c) 2022 Cysharp, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- diff --git a/NATS.Jwt.CheckNativeAot/NATS.Jwt.CheckNativeAot.csproj b/NATS.Jwt.CheckNativeAot/NATS.Jwt.CheckNativeAot.csproj new file mode 100644 index 0000000..842edaf --- /dev/null +++ b/NATS.Jwt.CheckNativeAot/NATS.Jwt.CheckNativeAot.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + true + false + true + + + + + + + diff --git a/NATS.Jwt.CheckNativeAot/Program.cs b/NATS.Jwt.CheckNativeAot/Program.cs new file mode 100644 index 0000000..a6b2324 --- /dev/null +++ b/NATS.Jwt.CheckNativeAot/Program.cs @@ -0,0 +1,59 @@ +using NATS.Jwt; +using NATS.NKeys; + +var jwt = new NatsJwt(); + +var okp = KeyPair.FromSeed("SOAMMC2AYOVIEEW6MRVS3ZC73G3KH5NW23GBB67E44STYPRPBTUC7DUKJU"); +var opk = okp.GetPublicKey(); + +var oc = jwt.NewOperatorClaims(opk); +oc.Name = "O"; + +var oskp = KeyPair.FromSeed("SOAIJKUDGENIQC7H3R3E55B6HAWSC223RJMZ6NJFTWRF5SVMUQ2CCQYJTI"); +var ospk = oskp.GetPublicKey(); +oc.Operator.SigningKeys = [ospk]; + +var operatorJwt = jwt.Encode(oc, okp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); +const string operatorJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3Q0haWEJDQ0FCTFZENk80VFJGRkcyWjJFUUtHT1A1MzZESFRaUkU0VVZTQ0lDTjZHS0VBIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJPQVVHVEg0VEQyNTNZS0RGTVBCVlFBNkxTNkQ0WlJST1RLRksyQkVHRkdPN0EyNVlPUVNUVFpHTiIsIm5hbWUiOiJPIiwic3ViIjoiT0FVR1RINFREMjUzWUtERk1QQlZRQTZMUzZENFpSUk9US0ZLMkJFR0ZHTzdBMjVZT1FTVFRaR04iLCJuYXRzIjp7InNpZ25pbmdfa2V5cyI6WyJPQ0dBQVFIWEJXN1RaN1BMUVZUNUhUU1JMVFVSNldXS1lXTkdESFpKSUZKVlVGM0dWQktZNjI2WiJdLCJ0eXBlIjoib3BlcmF0b3IiLCJ2ZXJzaW9uIjoyfX0.Kv8xA8FmO0XKC79pgEty-bmYCTKpKU6gJPby3OfMMbsUHY4qobdvrpsbrmroCNNZHjSCmwY0Y8Fs-AxO-gSUBQ"; +Assert.Equal(operatorJwt1, operatorJwt, "operatorJwt"); + +var akp = KeyPair.FromSeed("SAAIEBXNONQAZMHHGG4PAFAY5NNDOOFD5PKXG3JHCNWT2HGPVOPNBGLVXY"); +var apk = akp.GetPublicKey(); +var ac = jwt.NewAccountClaims(apk); +ac.Name = "A"; + +var askp = KeyPair.FromSeed("SAAOEJM7WOGBA6E67NAHP7TNGR2ABLKIGKA4EY264LIINLRJNRZJE2HOSU"); +var aspk = askp.GetPublicKey(); +ac.Account.SigningKeys = [aspk]; +var accountJwt = jwt.Encode(ac, oskp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); + +const string accountJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3STIzVDJGUEpLUU9LRkdTNVVJUjdOUVc0MlVNVEVYRkhISDdOSURYQkFZRVNDNFZFU1JRIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJPQ0dBQVFIWEJXN1RaN1BMUVZUNUhUU1JMVFVSNldXS1lXTkdESFpKSUZKVlVGM0dWQktZNjI2WiIsIm5hbWUiOiJBIiwic3ViIjoiQUFGSDNPVTRUUDIzQlZLTEFXUkpFQTNTM1RZSU9BQlI1NUJHQkRZT0g3NFVVTDZKNlBTSkhHSUIiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFBMjdGMjRJQVVES1M1T0RRUUU2S0xVQk5TVllIVU5OS0dJWTRWMkpXTDJEU1dKNFFQWlo3TE43Il0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sImF1dGhvcml6YXRpb24iOnt9LCJ0eXBlIjoiYWNjb3VudCIsInZlcnNpb24iOjJ9fQ.0dEQvGqCwZhjhCEfnSkhMbGfMtx-G9PxXIyaRjMvqPaTZahHRypH38tbLegJcmVPJ0GvmtqRFD95M5F_bXFsDQ"; +Assert.Equal(accountJwt1, accountJwt, "accountJwt"); + +const string apk1 = "AAFH3OU4TP23BVKLAWRJEA3S3TYIOABR55BGBDYOH74UUL6J6PSJHGIB"; +Assert.Equal(apk1, apk, "apk"); + +var ukp = KeyPair.FromSeed("SUAOT7W25IDZJYUCOMTNXZORZZCQM6HNYMCPYRAIP7JXAMWJT72IURZHBY"); +var upk = ukp.GetPublicKey(); +var uc = jwt.NewUserClaims(upk); +uc.User.IssuerAccount = apk; +var userJwt = jwt.Encode(uc, askp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); + +const string userJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJRWFBKRFBaNUFVWlBQWk5MWDJBVUI3M1lLU1VZQ0RRNEhVTkpNQ0dTREhaVDNNU1hERlNRIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJBQTI3RjI0SUFVREtTNU9EUVFFNktMVUJOU1ZZSFVOTktHSVk0VjJKV0wyRFNXSjRRUFpaN0xONyIsInN1YiI6IlVDTUpLUFJZR1kzUUNaQVhBVUJWWFhJSVIySElPVFJHS1FDTUFERkZHVjNORFZWUkxHNUpLSUxKIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpc3N1ZXJfYWNjb3VudCI6IkFBRkgzT1U0VFAyM0JWS0xBV1JKRUEzUzNUWUlPQUJSNTVCR0JEWU9INzRVVUw2SjZQU0pIR0lCIiwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.EOj8fO8TshAjXYUxvyg1msy4sg7T250_FK_Jd4qmsOXCu2LrI3-dKeXfj2W3LWegiSvJL09uC2FQ8LEWSHD5BA"; +Assert.Equal(userJwt1, userJwt, "userJwt"); + +const string userSeed1 = "SUAOT7W25IDZJYUCOMTNXZORZZCQM6HNYMCPYRAIP7JXAMWJT72IURZHBY"; +Assert.Equal(userSeed1, ukp.GetSeed(), "userSeed"); + +Console.WriteLine("PASS"); + +public static class Assert +{ + public static void Equal(string expected, string actual, string name) + { + if (!string.Equals(expected, actual)) + throw new Exception($"Strings are not equal ({name}).\n---\nExpected:\n{expected}\nActual:\n{actual}\n---"); + + Console.WriteLine($"OK {name}"); + } +} diff --git a/NATS.Jwt.Tests/ConnectTests.cs b/NATS.Jwt.Tests/ConnectTests.cs new file mode 100644 index 0000000..bb5213c --- /dev/null +++ b/NATS.Jwt.Tests/ConnectTests.cs @@ -0,0 +1,242 @@ +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using NATS.Client.Core; +using NATS.Jwt.Internal; +using NATS.Jwt.Models; +using NATS.NKeys; +using Xunit; +using Xunit.Abstractions; + +namespace NATS.Jwt.Tests; + +public class ConnectTests +{ + private readonly ITestOutputHelper _output; + + public ConnectTests(ITestOutputHelper output) => _output = output; + + [Fact] + public async Task Connect_with_pre_generated_JWTs() + { + const string operatorJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBRFJSSU1WTVE1Q0NQSkNYVVZaV1g1R1dONDJCREFVRDNUUUlaT0xNWE1SRFdUQkxWS1FRIiwiaWF0IjoxNzE5NTE5NzI3LCJpc3MiOiJPRFNYNldCTU5SRURCNEk2UzZOQkc3RFVNQ1hJVklFNlFBNDRRNEE0R0c1WlNEQU9DRENFV1A0USIsIm5hbWUiOiJPIiwic3ViIjoiT0RTWDZXQk1OUkVEQjRJNlM2TkJHN0RVTUNYSVZJRTZRQTQ0UTRBNEdHNVpTREFPQ0RDRVdQNFEiLCJuYXRzIjp7InNpZ25pbmdfa2V5cyI6WyJPQlVDRzNSRjQ0VkI0V1FZSEVNVEdKRzNOTUszNlgyNEg0RTVFRkpaSllXWVgzVlY1TDRKSUpTQiJdLCJ0eXBlIjoib3BlcmF0b3IiLCJ2ZXJzaW9uIjoyfX0.9xcbut1K6Lln8231cD0E7Wd9Sell-xEZr8XWbY6Ej4rzFXrAzkz1TUHDrTYm8G2xyEwxf3tykbRuGE-y1DLYDA"; + const string apk = "ABSMSCU7T4MJXN44NXZCAI52BY4QIL7SIKMSFL3LGRWMZLNCIGSTI7K7"; + const string accountJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJGVFVRMzJXWERDR0VaNldJVUdFR0NEM1dSRlJYR1MyQkpENkhQQ09PSTI2WTJMNlhIVlZRIiwiaWF0IjoxNzE5NTE5NzI3LCJpc3MiOiJPQlVDRzNSRjQ0VkI0V1FZSEVNVEdKRzNOTUszNlgyNEg0RTVFRkpaSllXWVgzVlY1TDRKSUpTQiIsIm5hbWUiOiJBIiwic3ViIjoiQUJTTVNDVTdUNE1KWE40NE5YWkNBSTUyQlk0UUlMN1NJS01TRkwzTEdSV01aTE5DSUdTVEk3SzciLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFCUVZLS1czUTVMUklWSlVORFBMWVZTUkxLRklJR0VLUUdZWkNQWVJEVkFZVFNDVU5DWVJUVDdYIl0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sImF1dGhvcml6YXRpb24iOnt9LCJ0eXBlIjoiYWNjb3VudCIsInZlcnNpb24iOjJ9fQ.vpBwb1JelL-ZpDY5QZX520upq2xs6Kq-4UKOhEA-llbSzXu9MgQpH8chmiZHIqOfwiRslNBxfFvmxjJBlikHAQ"; + const string userJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJLTzNMWlhHM0FEWkRER0tKU1Q0UUJZMkFPUlFPQzdGS1lMQ0lPTTdGT0hKSkFPMkVSV1RRIiwiaWF0IjoxNzE5NTE5NzI3LCJpc3MiOiJBQlFWS0tXM1E1TFJJVkpVTkRQTFlWU1JMS0ZJSUdFS1FHWVpDUFlSRFZBWVRTQ1VOQ1lSVFQ3WCIsInN1YiI6IlVBNktDNktCSVBHTklDMk4yRlZKTlFPV0dITjNXMkJITUtaSUtCSlVYRU41SU5JSVpKRlcyNVlGIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpc3N1ZXJfYWNjb3VudCI6IkFCU01TQ1U3VDRNSlhONDROWFpDQUk1MkJZNFFJTDdTSUtNU0ZMM0xHUldNWkxOQ0lHU1RJN0s3IiwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.eMkzuH_0qI2MHm97OmWL-v40BSq_fF5YJXNkZ71oFG6M6NVnFreXrLvb79nUOOj2Kln5O7LoufL_DWoiCtU9Cg"; + const string userSeed = "SUAOPJ35Z64IDGTSF5CPRXODVLKV4PXYOQ7SNUZMMNSNJVDPV3EF3PNZ2Y"; + + const string conf = $$""" + operator: {{operatorJwt}} + + resolver: MEMORY + resolver_preload: { + {{apk}}: {{accountJwt}} + } + """; + + const string confPath = $"server_{nameof(Connect_with_pre_generated_JWTs)}.conf"; + + File.WriteAllText(confPath, conf); + await using var server = await NatsServerProcess.StartAsync(config: confPath); + + // Connect as user + { + var authOpts = new NatsAuthOpts { Jwt = userJwt, Seed = userSeed }; + var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; + await using var nats = new NatsConnection(opts); + await nats.PingAsync(); + } + + // Wrong user credentials + var exception = await Assert.ThrowsAsync(async () => + { + var authOpts = new NatsAuthOpts { Jwt = userJwt }; + var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; + await using var nats = new NatsConnection(opts); + await nats.PingAsync(); + }); + _output.WriteLine($"{exception.GetType().Name}: {exception.Message}"); + _output.WriteLine($"{exception.InnerException?.GetType().Name}: {exception.InnerException?.Message}"); + Assert.Contains("Authorization Violation", exception.InnerException?.Message); + } + + [Fact] + public async Task Generate_JWTs() + { + var jwt = new NatsJwt(); + + var okp = KeyPair.FromSeed("SOAMMC2AYOVIEEW6MRVS3ZC73G3KH5NW23GBB67E44STYPRPBTUC7DUKJU".ToCharArray()); + var opk = okp.GetPublicKey(); + + var oc = jwt.NewOperatorClaims(opk); + oc.Name = "O"; + + var oskp = KeyPair.FromSeed("SOAIJKUDGENIQC7H3R3E55B6HAWSC223RJMZ6NJFTWRF5SVMUQ2CCQYJTI".ToCharArray()); + var ospk = oskp.GetPublicKey(); + oc.Operator.SigningKeys = [ospk]; + + var operatorJwt = jwt.Encode(oc, okp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); + const string operatorJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3Q0haWEJDQ0FCTFZENk80VFJGRkcyWjJFUUtHT1A1MzZESFRaUkU0VVZTQ0lDTjZHS0VBIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJPQVVHVEg0VEQyNTNZS0RGTVBCVlFBNkxTNkQ0WlJST1RLRksyQkVHRkdPN0EyNVlPUVNUVFpHTiIsIm5hbWUiOiJPIiwic3ViIjoiT0FVR1RINFREMjUzWUtERk1QQlZRQTZMUzZENFpSUk9US0ZLMkJFR0ZHTzdBMjVZT1FTVFRaR04iLCJuYXRzIjp7InNpZ25pbmdfa2V5cyI6WyJPQ0dBQVFIWEJXN1RaN1BMUVZUNUhUU1JMVFVSNldXS1lXTkdESFpKSUZKVlVGM0dWQktZNjI2WiJdLCJ0eXBlIjoib3BlcmF0b3IiLCJ2ZXJzaW9uIjoyfX0.Kv8xA8FmO0XKC79pgEty-bmYCTKpKU6gJPby3OfMMbsUHY4qobdvrpsbrmroCNNZHjSCmwY0Y8Fs-AxO-gSUBQ"; + Assert.Equal(operatorJwt1, operatorJwt); + + var akp = KeyPair.FromSeed("SAAIEBXNONQAZMHHGG4PAFAY5NNDOOFD5PKXG3JHCNWT2HGPVOPNBGLVXY".ToCharArray()); + var apk = akp.GetPublicKey(); + var ac = jwt.NewAccountClaims(apk); + ac.Name = "A"; + + var askp = KeyPair.FromSeed("SAAOEJM7WOGBA6E67NAHP7TNGR2ABLKIGKA4EY264LIINLRJNRZJE2HOSU".ToCharArray()); + var aspk = askp.GetPublicKey(); + ac.Account.SigningKeys = [aspk]; + var accountJwt = jwt.Encode(ac, oskp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); + + const string accountJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3STIzVDJGUEpLUU9LRkdTNVVJUjdOUVc0MlVNVEVYRkhISDdOSURYQkFZRVNDNFZFU1JRIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJPQ0dBQVFIWEJXN1RaN1BMUVZUNUhUU1JMVFVSNldXS1lXTkdESFpKSUZKVlVGM0dWQktZNjI2WiIsIm5hbWUiOiJBIiwic3ViIjoiQUFGSDNPVTRUUDIzQlZLTEFXUkpFQTNTM1RZSU9BQlI1NUJHQkRZT0g3NFVVTDZKNlBTSkhHSUIiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFBMjdGMjRJQVVES1M1T0RRUUU2S0xVQk5TVllIVU5OS0dJWTRWMkpXTDJEU1dKNFFQWlo3TE43Il0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sImF1dGhvcml6YXRpb24iOnt9LCJ0eXBlIjoiYWNjb3VudCIsInZlcnNpb24iOjJ9fQ.0dEQvGqCwZhjhCEfnSkhMbGfMtx-G9PxXIyaRjMvqPaTZahHRypH38tbLegJcmVPJ0GvmtqRFD95M5F_bXFsDQ"; + Assert.Equal(accountJwt1, accountJwt); + + const string apk1 = "AAFH3OU4TP23BVKLAWRJEA3S3TYIOABR55BGBDYOH74UUL6J6PSJHGIB"; + Assert.Equal(apk1, apk); + + var ukp = KeyPair.FromSeed("SUAOT7W25IDZJYUCOMTNXZORZZCQM6HNYMCPYRAIP7JXAMWJT72IURZHBY".ToCharArray()); + var upk = ukp.GetPublicKey(); + var uc = jwt.NewUserClaims(upk); + uc.User.IssuerAccount = apk; + var userJwt = jwt.Encode(uc, askp, DateTimeOffset.FromUnixTimeSeconds(1720720359)); + + const string userJwt1 = "eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJRWFBKRFBaNUFVWlBQWk5MWDJBVUI3M1lLU1VZQ0RRNEhVTkpNQ0dTREhaVDNNU1hERlNRIiwiaWF0IjoxNzIwNzIwMzU5LCJpc3MiOiJBQTI3RjI0SUFVREtTNU9EUVFFNktMVUJOU1ZZSFVOTktHSVk0VjJKV0wyRFNXSjRRUFpaN0xONyIsInN1YiI6IlVDTUpLUFJZR1kzUUNaQVhBVUJWWFhJSVIySElPVFJHS1FDTUFERkZHVjNORFZWUkxHNUpLSUxKIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpc3N1ZXJfYWNjb3VudCI6IkFBRkgzT1U0VFAyM0JWS0xBV1JKRUEzUzNUWUlPQUJSNTVCR0JEWU9INzRVVUw2SjZQU0pIR0lCIiwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.EOj8fO8TshAjXYUxvyg1msy4sg7T250_FK_Jd4qmsOXCu2LrI3-dKeXfj2W3LWegiSvJL09uC2FQ8LEWSHD5BA"; + // Assert.Equal(userJwt1, userJwt); + + const string userSeed1 = "SUAOT7W25IDZJYUCOMTNXZORZZCQM6HNYMCPYRAIP7JXAMWJT72IURZHBY"; + Assert.Equal(userSeed1, ukp.GetSeed()); + } + + [Fact] + public async Task Connect_with_generated_JWTs() + { + var jwt = new NatsJwt(); + + var okp = KeyPair.CreatePair(PrefixByte.Operator); + var opk = okp.GetPublicKey(); + + var oc = jwt.NewOperatorClaims(opk); + oc.Name = "O"; + + var oskp = KeyPair.CreatePair(PrefixByte.Operator); + var ospk = oskp.GetPublicKey(); + oc.Operator.SigningKeys = [ospk]; + var operatorJwt = jwt.Encode(oc, okp); + + var akp = KeyPair.CreatePair(PrefixByte.Account); + var apk = akp.GetPublicKey(); + var ac = jwt.NewAccountClaims(apk); + ac.Name = "A"; + + var askp = KeyPair.CreatePair(PrefixByte.Account); + var aspk = askp.GetPublicKey(); + ac.Account.SigningKeys = [aspk]; + var accountJwt = jwt.Encode(ac, oskp); + + var ukp = KeyPair.CreatePair(PrefixByte.User); + var upk = ukp.GetPublicKey(); + var uc = jwt.NewUserClaims(upk); + uc.User.IssuerAccount = apk; + var userJwt = jwt.Encode(uc, askp); + + var userSeed = ukp.GetSeed(); + + var conf = $$""" + operator: {{operatorJwt}} + + resolver: MEMORY + resolver_preload: { + {{apk}}: {{accountJwt}} + } + """; + + const string confPath = $"server_{nameof(Connect_with_generated_JWTs)}.conf"; + + File.WriteAllText(confPath, conf); + await using var server = await NatsServerProcess.StartAsync(config: confPath); + + // Connect as user + { + var authOpts = new NatsAuthOpts { Jwt = userJwt, Seed = userSeed }; + var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; + await using var nats = new NatsConnection(opts); + await nats.PingAsync(); + } + + // Wrong user credentials + var exception = await Assert.ThrowsAsync(async () => + { + var authOpts = new NatsAuthOpts { Jwt = userJwt }; + var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; + await using var nats = new NatsConnection(opts); + await nats.PingAsync(); + }); + _output.WriteLine($"{exception.GetType().Name}: {exception.Message}"); + _output.WriteLine($"{exception.InnerException?.GetType().Name}: {exception.InnerException?.Message}"); + Assert.Contains("Authorization Violation", exception.InnerException?.Message); + } + + [Fact] + public async Task Readme_example() + { + var jwt = new NatsJwt(); + + var okp = KeyPair.CreatePair(PrefixByte.Operator); + var opk = okp.GetPublicKey(); + + var oc = jwt.NewOperatorClaims(opk); + oc.Name = "Example Operator"; + + var oskp = KeyPair.CreatePair(PrefixByte.Operator); + var ospk = oskp.GetPublicKey(); + oc.Operator.SigningKeys = [ospk]; + var operatorJwt = jwt.Encode(oc, okp); + + var akp = KeyPair.CreatePair(PrefixByte.Account); + var apk = akp.GetPublicKey(); + var ac = jwt.NewAccountClaims(apk); + ac.Name = "Example Org"; + + var askp = KeyPair.CreatePair(PrefixByte.Account); + var aspk = askp.GetPublicKey(); + ac.Account.SigningKeys = [aspk]; + var accountJwt = jwt.Encode(ac, oskp); + + var ukp = KeyPair.CreatePair(PrefixByte.User); + var upk = ukp.GetPublicKey(); + var uc = jwt.NewUserClaims(upk); + uc.User.IssuerAccount = apk; + var userJwt = jwt.Encode(uc, askp); + + var userSeed = ukp.GetSeed(); + + var conf = $$""" + operator: {{operatorJwt}} + + resolver: MEMORY + resolver_preload: { + {{apk}}: {{accountJwt}} + } + """; + + const string confPath = $"example_server.conf"; + File.WriteAllText(confPath, conf); + await using var server = await NatsServerProcess.StartAsync(config: confPath); + + const string credsPath = $"example_user.creds"; + File.WriteAllText(credsPath, jwt.FormatUserConfig(userJwt, userSeed)); + + // Connect as user + { + var authOpts = new NatsAuthOpts { CredsFile = credsPath }; + var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; + await using var nats = new NatsConnection(opts); + await nats.PingAsync(); + } + } +} diff --git a/NATS.Jwt.Tests/NATS.Jwt.Tests.csproj b/NATS.Jwt.Tests/NATS.Jwt.Tests.csproj new file mode 100644 index 0000000..7b1f27c --- /dev/null +++ b/NATS.Jwt.Tests/NATS.Jwt.Tests.csproj @@ -0,0 +1,32 @@ + + + + net462;net481;net6.0;net8.0 + net6.0;net8.0 + false + true + $(NoWarn);VSTHRD111;VSTHRD200 + 0 + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/NATS.Jwt.Tests/NatsGenericClaimsTests.cs b/NATS.Jwt.Tests/NatsGenericClaimsTests.cs new file mode 100644 index 0000000..1f530ce --- /dev/null +++ b/NATS.Jwt.Tests/NatsGenericClaimsTests.cs @@ -0,0 +1,108 @@ +using System; +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using JsonDiffPatchDotNet; +using NATS.Jwt.Internal; +using NATS.Jwt.Models; +using Newtonsoft.Json.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace NATS.Jwt.Tests; + +public class NatsGenericClaimsTests +{ + private readonly ITestOutputHelper _output; + + public NatsGenericClaimsTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Generic_claims_can_serialize_data() + { + var claims = new NatsGenericClaims + { + Subject = "1234567890", + Data = new() + { + ["user"] = new JsonObject + { + ["name"] = "John Doe", + ["email"] = "john@example.com", + ["roles"] = new JsonArray { "admin", "user" }, + }, + }, + }; + var writer = new SimpleBufferWriter(1024); + var jsonWriter = new Utf8JsonWriter(writer); + JsonSerializer.Serialize(jsonWriter, claims, JsonContext.Default.NatsGenericClaims); + var json = Encoding.ASCII.GetString(writer.WrittenMemory.ToArray()); + + _output.WriteLine($"json: {json}"); + + var jsonReader = new Utf8JsonReader(writer.WrittenMemory.Span); + var claims2 = JsonSerializer.Deserialize(ref jsonReader, JsonContext.Default.NatsGenericClaims); + _output.WriteLine($"claims2: {claims2}"); + + var json2 = JsonSerializer.Serialize(claims2); + _output.WriteLine($"json2: {json2}"); + + var jdp = new JsonDiffPatch(); + var left = JToken.Parse(json); + var right = JToken.Parse(json2); + var patch = jdp.Diff(left, right); + _output.WriteLine($"patch: {patch}"); + Assert.Null(patch); + } +} + +public class SimpleBufferWriter : IBufferWriter +{ + private byte[] _buffer; + private int _index; + + public SimpleBufferWriter(int initialCapacity) + { + _buffer = new byte[initialCapacity]; + _index = 0; + } + + public ReadOnlyMemory WrittenMemory => _buffer.AsMemory(0, _index); + + public void Advance(int count) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = 0) + { + EnsureCapacity(sizeHint); + return _buffer.AsSpan(_index); + } + + private void EnsureCapacity(int sizeHint) + { + if (sizeHint < 0) + sizeHint = 0; + + var availableSpace = _buffer.Length - _index; + + if (sizeHint <= availableSpace) + return; + var growBy = Math.Max(sizeHint, _buffer.Length); + Array.Resize(ref _buffer, checked(_buffer.Length + growBy)); + } +} diff --git a/NATS.Jwt.Tests/NatsJwtTests.cs b/NATS.Jwt.Tests/NatsJwtTests.cs new file mode 100644 index 0000000..6dd7206 --- /dev/null +++ b/NATS.Jwt.Tests/NatsJwtTests.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using NATS.NKeys; +using Xunit; +using Xunit.Abstractions; + +namespace NATS.Jwt.Tests; + +public class NatsJwtTests +{ + private readonly ITestOutputHelper _output; + + public NatsJwtTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void T() + { + var okp = KeyPair.CreatePair(PrefixByte.Operator); + var opk = okp.GetPublicKey(); + var jwt = new NatsJwt(); + var oc = jwt.NewOperatorClaims(opk); + oc.Name = "O"; + + var oskp = KeyPair.CreatePair(PrefixByte.Operator); + var ospk = oskp.GetPublicKey(); + + oc.Operator.SigningKeys = [ospk]; + } +} diff --git a/NATS.Jwt.Tests/NatsServerProcess.cs b/NATS.Jwt.Tests/NatsServerProcess.cs new file mode 100644 index 0000000..e3ba131 --- /dev/null +++ b/NATS.Jwt.Tests/NatsServerProcess.cs @@ -0,0 +1,289 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Exception = System.Exception; + +#pragma warning disable VSTHRD103 +#pragma warning disable VSTHRD105 +#pragma warning disable SA1512 + +// ReSharper disable SuggestVarOrType_BuiltInTypes +// ReSharper disable SuggestVarOrType_SimpleTypes +// ReSharper disable NotAccessedField.Local +// ReSharper disable UseObjectOrCollectionInitializer +// ReSharper disable InconsistentNaming + +namespace NATS.Jwt.Tests; + +public class NatsServerProcess : IAsyncDisposable +{ + private readonly Action _logger; + private readonly Process _process; + private readonly string _scratch; + + private NatsServerProcess(Action logger, Process process, string url, string scratch) + { + Url = url; + _logger = logger; + _process = process; + _scratch = scratch; + } + + public string Url { get; } + + public static async ValueTask StartAsync(Action? logger = null, string? config = null) + { + var isLoggingEnabled = logger != null; + var log = logger ?? (_ => { }); + + var scratch = Path.Combine(Path.GetTempPath(), "nats.net.tests", Guid.NewGuid().ToString()); + + var portsFileDir = Path.Combine(scratch, "port"); + Directory.CreateDirectory(portsFileDir); + + var natsServerExe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "nats-server.exe" : "nats-server"; + var configFlag = config == null ? string.Empty : $"-c \"{config}\""; + var portsFileDirEsc = portsFileDir.Replace(@"\", "/"); + var info = new ProcessStartInfo + { + FileName = natsServerExe, + Arguments = $"{configFlag} -a 127.0.0.1 -p -1 --ports_file_dir \"{portsFileDirEsc}\"", + UseShellExecute = false, + CreateNoWindow = false, + RedirectStandardError = isLoggingEnabled, + RedirectStandardOutput = isLoggingEnabled, + }; + var process = new Process { StartInfo = info, }; + + if (isLoggingEnabled) + { + DataReceivedEventHandler outputHandler = (_, e) => log(e.Data); + process.OutputDataReceived += outputHandler; + process.ErrorDataReceived += outputHandler; + } + + process.Start(); + + ChildProcessTracker.AddProcess(process); + + if (isLoggingEnabled) + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + } + + var portsFile = Path.Combine(portsFileDir, $"{natsServerExe}_{process.Id}.ports"); + log($"portsFile={portsFile}"); + + string? ports = null; + Exception? exception = null; + for (var i = 0; i < 10; i++) + { + try + { + ports = File.ReadAllText(portsFile); + break; + } + catch (Exception e) + { + exception = e; + await Task.Delay(100 + (500 * i)); + } + } + + if (ports == null) + { + throw exception ?? new Exception("Failed to read ports file."); + } + + var url = Regex.Match(ports, @"\w+://[\d\.]+:\d+").Groups[0].Value; + log($"ports={ports}"); + log($"url={url}"); + + return new NatsServerProcess(log, process, url, scratch); + } + + public async ValueTask DisposeAsync() + { + try + { + _process.Kill(); + } + catch + { + // best effort + } + + for (var i = 0; i < 3; i++) + { + try + { + Directory.Delete(_scratch, recursive: true); + break; + } + catch + { + await Task.Delay(100); + } + } + + _process.Dispose(); + } +} + +// Borrowed from https://stackoverflow.com/questions/3342941/kill-child-process-when-parent-process-is-killed/37034966#37034966 + +/// +/// Allows processes to be automatically killed if this parent process unexpectedly quits. +/// This feature requires Windows 8 or greater. On Windows 7, nothing is done. +/// References: +/// https://stackoverflow.com/a/4657392/386091 +/// https://stackoverflow.com/a/9164742/386091 +#pragma warning disable SA1204 +#pragma warning disable SA1129 +#pragma warning disable SA1201 +#pragma warning disable SA1117 +#pragma warning disable SA1400 +#pragma warning disable SA1311 +#pragma warning disable SA1308 +#pragma warning disable SA1413 +#pragma warning disable SA1121 +public static class ChildProcessTracker +{ + /// + /// Add the process to be tracked. If our current process is killed, the child processes + /// that we are tracking will be automatically killed, too. If the child process terminates + /// first, that's fine, too. + /// + public static void AddProcess(Process process) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + if (s_jobHandle != IntPtr.Zero) + { + var success = AssignProcessToJobObject(s_jobHandle, process.Handle); + if (!success && !process.HasExited) + throw new Win32Exception(); + } + } + + static ChildProcessTracker() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return; + + // This feature requires Windows 8 or later. To support Windows 7, requires + // registry settings to be added if you are using Visual Studio plus an + // app.manifest change. + // https://stackoverflow.com/a/4232259/386091 + // https://stackoverflow.com/a/9507862/386091 + if (Environment.OSVersion.Version < new Version(6, 2)) + return; + + // The job name is optional (and can be null), but it helps with diagnostics. + // If it's not null, it has to be unique. Use SysInternals' Handle command-line + // utility: handle -a ChildProcessTracker + var jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id; + s_jobHandle = CreateJobObject(IntPtr.Zero, jobName); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); + + // This is the key flag. When our process is killed, Windows will automatically + // close the job handle, and when that happens, we want the child processes to + // be killed, too. + info.LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); + extendedInfo.BasicLimitInformation = info; + + var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + var extendedInfoPtr = Marshal.AllocHGlobal(length); + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation, + extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, + IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + // Windows will automatically close any open job handles when our process terminates. + // This can be verified by using SysInternals' Handle utility. When the job handle + // is closed, the child processes will be killed. + private static readonly IntPtr s_jobHandle; +} + +public enum JobObjectInfoType +{ + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 +} + +[StructLayout(LayoutKind.Sequential)] +public struct JOBOBJECT_BASIC_LIMIT_INFORMATION +{ + public long PerProcessUserTimeLimit; + public long PerJobUserTimeLimit; + public JOBOBJECTLIMIT LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public uint ActiveProcessLimit; + public long Affinity; + public uint PriorityClass; + public uint SchedulingClass; +} + +[Flags] +public enum JOBOBJECTLIMIT : uint +{ + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 +} + +[StructLayout(LayoutKind.Sequential)] +public struct IO_COUNTERS +{ + public ulong ReadOperationCount; + public ulong WriteOperationCount; + public ulong OtherOperationCount; + public ulong ReadTransferCount; + public ulong WriteTransferCount; + public ulong OtherTransferCount; +} + +[StructLayout(LayoutKind.Sequential)] +public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION +{ + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; +} diff --git a/NATS.Jwt.sln b/NATS.Jwt.sln new file mode 100644 index 0000000..1d698b2 --- /dev/null +++ b/NATS.Jwt.sln @@ -0,0 +1,67 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Jwt", "NATS.Jwt\NATS.Jwt.csproj", "{9B613C47-922B-4AA8-82D7-1520A13E0F74}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Jwt.Tests", "NATS.Jwt.Tests\NATS.Jwt.Tests.csproj", "{707139DF-D75B-40F8-9AA7-816C64E98572}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".", ".", "{1F24478C-D5CB-4A58-A74E-6371F7F95C01}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + LICENSE = LICENSE + Directory.Build.props = Directory.Build.props + Icon.png = Icon.png + version.txt = version.txt + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + Default.runsettings = Default.runsettings + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{03E5999C-5D20-4EA2-B8C8-53ACE65B221F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{1EB01F81-8857-4646-B8BF-16024F357506}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml + .github\ISSUE_TEMPLATE\defect.yml = .github\ISSUE_TEMPLATE\defect.yml + .github\ISSUE_TEMPLATE\proposal.yml = .github\ISSUE_TEMPLATE\proposal.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{9C98B59B-D0C0-4F2B-8333-2242ED1B5F70}" + ProjectSection(SolutionItems) = preProject + .github\workflows\format.yml = .github\workflows\format.yml + .github\workflows\release.yml = .github\workflows\release.yml + .github\workflows\test.yml = .github\workflows\test.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NATS.Jwt.CheckNativeAot", "NATS.Jwt.CheckNativeAot\NATS.Jwt.CheckNativeAot.csproj", "{E144211D-3549-4E78-BFC4-B5BF7E70AEDA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9B613C47-922B-4AA8-82D7-1520A13E0F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B613C47-922B-4AA8-82D7-1520A13E0F74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B613C47-922B-4AA8-82D7-1520A13E0F74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B613C47-922B-4AA8-82D7-1520A13E0F74}.Release|Any CPU.Build.0 = Release|Any CPU + {707139DF-D75B-40F8-9AA7-816C64E98572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {707139DF-D75B-40F8-9AA7-816C64E98572}.Debug|Any CPU.Build.0 = Debug|Any CPU + {707139DF-D75B-40F8-9AA7-816C64E98572}.Release|Any CPU.ActiveCfg = Release|Any CPU + {707139DF-D75B-40F8-9AA7-816C64E98572}.Release|Any CPU.Build.0 = Release|Any CPU + {E144211D-3549-4E78-BFC4-B5BF7E70AEDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E144211D-3549-4E78-BFC4-B5BF7E70AEDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E144211D-3549-4E78-BFC4-B5BF7E70AEDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E144211D-3549-4E78-BFC4-B5BF7E70AEDA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1EB01F81-8857-4646-B8BF-16024F357506} = {03E5999C-5D20-4EA2-B8C8-53ACE65B221F} + {9C98B59B-D0C0-4F2B-8333-2242ED1B5F70} = {03E5999C-5D20-4EA2-B8C8-53ACE65B221F} + EndGlobalSection +EndGlobal diff --git a/NATS.Jwt.sln.DotSettings b/NATS.Jwt.sln.DotSettings new file mode 100644 index 0000000..3ce9f44 --- /dev/null +++ b/NATS.Jwt.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/NATS.Jwt.snk b/NATS.Jwt.snk new file mode 100644 index 0000000000000000000000000000000000000000..549544c207778e63bedb3110d367470dabd11a3a GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097{X;g`fWcf=sh2D?!jP$wBeC0WY0@@aU z@m@X5!PP~djug)yH=icky z8bcoCJuBa2L67-l{5mXF9yCj*n=o)qk!pvU`o-t2r7;O+tnyW{NRawV{f?m(jDCNec$4*Uh}DATF-AR(jul0>HMu@N?^!weM3a?NlO$_0 zFRPm&MgoR!J5RUK(_N<1R0`)j@g`_78#&gEnn9@oLTNl3>1Jx*oc8HuMQLUgqXWjV$gekPyX=ay{ZA9^W3kQg4!Wp5BT}_%B zh1n%h!fLn}>^03D2X4Az=dpBxAl~wzNSj$R4pf#A_X->%65Nu3So%ZZ1@Ax7U@-s| zXz)c)b@8Z?qUQXk-x4gkU{@140w2XVu-fdIAkXHNRE2aNuy?tyv5jhW?i$L1T+p9S zJ&H)~f<`1*-();`C{D-lVxN(tc>F(A*G=&}znemTc1*^?_j?ED?#KmUNenm62!sO# icE%2=G_-{v+virfn1d + writer.WriteNumberValue((long)(value.TotalMilliseconds * 1_000_000L)); +} + +internal class NatsJSJsonNullableNanosecondsConverter : JsonConverter +{ + private readonly NatsJsJsonNanosecondsConverter _converter = new(); + + public override TimeSpan? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + return _converter.Read(ref reader, typeToConvert, options); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + _converter.Write(writer, value.Value, options); + } + } +} diff --git a/NATS.Jwt/Internal/NatsBufferWriter.cs b/NATS.Jwt/Internal/NatsBufferWriter.cs new file mode 100644 index 0000000..0489134 --- /dev/null +++ b/NATS.Jwt/Internal/NatsBufferWriter.cs @@ -0,0 +1,426 @@ +// adapted from https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.HighPerformance/Buffers/NatsBufferWriter%7BT%7D.cs + +using System; +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if !NETSTANDARD +using BitOperations = System.Numerics.BitOperations; +#endif + +namespace NATS.Jwt.Internal; + +/// +/// Represents a heap-based, array-backed output sink into which data can be written. +/// +/// The type of items to write to the current instance. +internal sealed class NatsBufferWriter : IBufferWriter, IMemoryOwner +{ + /// + /// The default buffer size to use to expand empty arrays. + /// + private const int DefaultInitialBufferSize = 256; + + /// + /// The instance used to rent . + /// + private readonly ArrayPool _pool; + + /// + /// The underlying array. + /// + private T[]? _array; + +#pragma warning disable IDE0032 // Use field over auto-property (clearer and faster) + /// + /// The starting offset within . + /// + private int _index; +#pragma warning restore IDE0032 + + /// + /// Initializes a new instance of the class. + /// + public NatsBufferWriter() + : this(ArrayPool.Shared, DefaultInitialBufferSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use. + public NatsBufferWriter(ArrayPool pool) + : this(pool, DefaultInitialBufferSize) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The minimum capacity with which to initialize the underlying buffer. + public NatsBufferWriter(int initialCapacity) + : this(ArrayPool.Shared, initialCapacity) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use. + /// The minimum capacity with which to initialize the underlying buffer. + /// Thrown when is not valid. + public NatsBufferWriter(ArrayPool pool, int initialCapacity) + { + // Since we're using pooled arrays, we can rent the buffer with the + // default size immediately, we don't need to use lazy initialization + // to save unnecessary memory allocations in this case. + // Additionally, we don't need to manually throw the exception if + // the requested size is not valid, as that'll be thrown automatically + // by the array pool in use when we try to rent an array with that size. + _pool = pool; + _array = pool.Rent(initialCapacity); + _index = 0; + } + + /// + Memory IMemoryOwner.Memory + { + // This property is explicitly implemented so that it's hidden + // under normal usage, as the name could be confusing when + // displayed besides WrittenMemory and GetMemory(). + // The IMemoryOwner interface is implemented primarily + // so that the AsStream() extension can be used on this type, + // allowing users to first create a NatsBufferWriter + // instance to write data to, then get a stream through the + // extension and let it take care of returning the underlying + // buffer to the shared pool when it's no longer necessary. + // Inlining is not needed here since this will always be a callvirt. + get => MemoryMarshal.AsMemory(WrittenMemory); + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public ReadOnlyMemory WrittenMemory + { + get + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsMemory(0, _index); + } + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public ReadOnlySpan WrittenSpan + { + get + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.AsSpan(0, _index); + } + } + + /// + /// Gets the amount of data written to the underlying buffer so far. + /// + public int WrittenCount + { + get => _index; + } + + /// + /// Gets the total amount of space within the underlying buffer. + /// + public int Capacity + { + get + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length; + } + } + + /// + /// Gets the amount of space available that can still be written into without forcing the underlying buffer to grow. + /// + public int FreeCapacity + { + get + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array!.Length - _index; + } + } + + /// + /// Clears the data written to the underlying buffer. + /// + /// + /// You must clear the buffer instance before trying to re-use it. + /// + public void Clear() + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + array.AsSpan(0, _index).Clear(); + + _index = 0; + } + + /// + public void Advance(int count) + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + if (count < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeCount(); + } + + if (_index > array!.Length - count) + { + ThrowArgumentExceptionForAdvancedTooFar(); + } + + _index += count; + } + + /// + public Memory GetMemory(int sizeHint = 0) + { + CheckBufferAndEnsureCapacity(sizeHint); + + return _array.AsMemory(_index); + } + + /// + public Span GetSpan(int sizeHint = 0) + { + CheckBufferAndEnsureCapacity(sizeHint); + + return _array.AsSpan(_index); + } + + /// + /// Gets an instance wrapping the underlying array in use. + /// + /// An instance wrapping the underlying array in use. + /// Thrown when the buffer in use has already been disposed. + /// + /// This method is meant to be used when working with APIs that only accept an array as input, and should be used with caution. + /// In particular, the returned array is rented from an array pool, and it is responsibility of the caller to ensure that it's + /// not used after the current instance is disposed. Doing so is considered undefined + /// behavior, as the same array might be in use within another instance. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ArraySegment DangerousGetArray() + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return new(array!, 0, _index); + } + + /// + public void Dispose() + { + var array = _array; + + if (array is null) + { + return; + } + + _array = null; + + _pool.Return(array); + } + + /// + public override string ToString() + { + // See comments in MemoryOwner about this + if (typeof(T) == typeof(char) && + _array is char[] chars) + { + return new(chars, 0, _index); + } + + // Same representation used in Span + return $"CommunityToolkit.HighPerformance.Buffers.NatsBufferWriter<{typeof(T)}>[{_index}]"; + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeCount() + { + throw new ArgumentOutOfRangeException("count", "The count can't be a negative value."); + } + + /// + /// Throws an when the size hint is negative. + /// + private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint() + { + throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value."); + } + + /// + /// Throws an when the requested count is negative. + /// + private static void ThrowArgumentExceptionForAdvancedTooFar() + { + throw new ArgumentException("The buffer writer has advanced too far."); + } + + /// + /// Throws an when is . + /// + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException("The current buffer has already been disposed."); + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckBufferAndEnsureCapacity(int sizeHint) + { + var array = _array; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + if (sizeHint < 0) + { + ThrowArgumentOutOfRangeExceptionForNegativeSizeHint(); + } + + if (sizeHint == 0) + { + sizeHint = 1; + } + + if (sizeHint > array!.Length - _index) + { + ResizeBuffer(sizeHint); + } + } + + /// + /// Resizes to ensure it can fit the specified number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + var minimumSize = (uint)_index + (uint)sizeHint; + +#if !NETSTANDARD + // The ArrayPool class has a maximum threshold of 1024 * 1024 for the maximum length of + // pooled arrays, and once this is exceeded it will just allocate a new array every time + // of exactly the requested size. In that case, we manually round up the requested size to + // the nearest power of two, to ensure that repeated consecutive writes when the array in + // use is bigger than that threshold don't end up causing a resize every single time. + if (minimumSize > 1024 * 1024) + { + minimumSize = BitOperations.RoundUpToPowerOf2(minimumSize); + } +#endif + + _pool.Resize(ref _array, (int)minimumSize); + } +} + +#pragma warning disable SA1204 +internal static class NatsBufferWriterExtensions +#pragma warning restore SA1204 +{ + /// + /// Changes the number of elements of a rented one-dimensional array to the specified new size. + /// + /// The type of items into the target array to resize. + /// The target instance to use to resize the array. + /// The rented array to resize, or to create a new array. + /// The size of the new array. + /// Indicates whether the contents of the array should be cleared before reuse. + /// Thrown when is less than 0. + /// When this method returns, the caller must not use any references to the old array anymore. + public static void Resize(this ArrayPool pool, [NotNull] ref T[]? array, int newSize, bool clearArray = false) + { + // If the old array is null, just create a new one with the requested size + if (array is null) + { + array = pool.Rent(newSize); + + return; + } + + // If the new size is the same as the current size, do nothing + if (array.Length == newSize) + { + return; + } + + // Rent a new array with the specified size, and copy as many items from the current array + // as possible to the new array. This mirrors the behavior of the Array.Resize API from + // the BCL: if the new size is greater than the length of the current array, copy all the + // items from the original array into the new one. Otherwise, copy as many items as possible, + // until the new array is completely filled, and ignore the remaining items in the first array. + var newArray = pool.Rent(newSize); + var itemsToCopy = Math.Min(array.Length, newSize); + + Array.Copy(array, 0, newArray, 0, itemsToCopy); + + pool.Return(array, clearArray); + + array = newArray; + } +} diff --git a/NATS.Jwt/Internal/SHA512256.cs b/NATS.Jwt/Internal/SHA512256.cs new file mode 100644 index 0000000..7b09741 --- /dev/null +++ b/NATS.Jwt/Internal/SHA512256.cs @@ -0,0 +1,187 @@ +// ReSharper disable all +#pragma warning disable + +using System; +using System.Text; + +namespace NATS.Jwt.Internal; + +internal static class Sha512256 +{ + private static readonly ulong[] K = + [ + 0x428a2f98d728ae22, + 0x7137449123ef65cd, + 0xb5c0fbcfec4d3b2f, + 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, + 0x59f111f1b605d019, + 0x923f82a4af194f9b, + 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, + 0x12835b0145706fbe, + 0x243185be4ee4b28c, + 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, + 0x80deb1fe3b1696b1, + 0x9bdc06a725c71235, + 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, + 0xefbe4786384f25e3, + 0x0fc19dc68b8cd5b5, + 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, + 0x4a7484aa6ea6e483, + 0x5cb0a9dcbd41fbd4, + 0x76f988da831153b5, + 0x983e5152ee66dfab, + 0xa831c66d2db43210, + 0xb00327c898fb213f, + 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, + 0xd5a79147930aa725, + 0x06ca6351e003826f, + 0x142929670a0e6e70, + 0x27b70a8546d22ffc, + 0x2e1b21385c26c926, + 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, + 0x650a73548baf63de, + 0x766a0abb3c77b2a8, + 0x81c2c92e47edaee6, + 0x92722c851482353b, + 0xa2bfe8a14cf10364, + 0xa81a664bbc423001, + 0xc24b8b70d0f89791, + 0xc76c51a30654be30, + 0xd192e819d6ef5218, + 0xd69906245565a910, + 0xf40e35855771202a, + 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, + 0x1e376c085141ab53, + 0x2748774cdf8eeb99, + 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, + 0x4ed8aa4ae3418acb, + 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, + 0x78a5636f43172f60, + 0x84c87814a1f0ab72, + 0x8cc702081a6439ec, + 0x90befffa23631e28, + 0xa4506cebde82bde9, + 0xbef9a3f7b2c67915, + 0xc67178f2e372532b, + 0xca273eceea26619c, + 0xd186b8c721c0c207, + 0xeada7dd6cde0eb1e, + 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, + 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, + 0x1b710b35131c471b, + 0x28db77f523047d84, + 0x32caab7b40c72493, + 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, + 0x597f299cfc657e2a, + 0x5fcb6fab3ad6faec, + 0x6c44198c4a475817 + ]; + + public static byte[] ComputeHash(byte[] message) + { + ulong[] H = new ulong[8] { + 0x22312194FC2BF72C, 0x9F555FA3C84C64C2, 0x2393B86B6F53B151, 0x963877195940EABD, + 0x96283EE2A88EFFE3, 0xBE5E1E2553863992, 0x2B0199FC2C85B8AA, 0x0EB72DDC81C52CA2 + }; + + int paddedLength = ((message.Length + 17) / 128 + 1) * 128; + byte[] padded = new byte[paddedLength]; + Array.Copy(message, padded, message.Length); + padded[message.Length] = 0x80; + + ulong bitLength = (ulong)message.Length * 8; + for (int i = 0; i < 8; i++) + padded[paddedLength - 8 + i] = (byte)(bitLength >> (56 - 8 * i)); + + for (int i = 0; i < paddedLength; i += 128) + { + ulong[] W = new ulong[80]; + for (int t = 0; t < 16; t++) + // W[t] = BitConverter.Toulong(padded, i + t * 8).ReverseEndianness(); + W[t] = SwapEndianness(BitConverter.ToUInt64(padded, i + t * 8)); + + for (int t = 16; t < 80; t++) + W[t] = sigma1(W[t - 2]) + W[t - 7] + sigma0(W[t - 15]) + W[t - 16]; + + ulong a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7]; + + for (int t = 0; t < 80; t++) + { + ulong T1 = h + Sigma1(e) + Ch(e, f, g) + K[t] + W[t]; + ulong T2 = Sigma0(a) + Maj(a, b, c); + h = g; + g = f; + f = e; + e = d + T1; + d = c; + c = b; + b = a; + a = T1 + T2; + } + + H[0] += a; + H[1] += b; + H[2] += c; + H[3] += d; + H[4] += e; + H[5] += f; + H[6] += g; + H[7] += h; + } + + byte[] hash = new byte[32]; + for (int i = 0; i < 4; i++) + { + ulong value = SwapEndianness(H[i]); + BitConverter.GetBytes(value).CopyTo(hash, i * 8); + } + + return hash; + } + + public static string ComputeHashString(string input) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(input); + byte[] hashBytes = ComputeHash(inputBytes); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLower(); + } + + private static ulong Ch(ulong x, ulong y, ulong z) => (x & y) ^ (~x & z); + + private static ulong Maj(ulong x, ulong y, ulong z) => (x & y) ^ (x & z) ^ (y & z); + + private static ulong RotR(ulong x, int n) => (x >> n) | (x << (64 - n)); + + private static ulong Sigma0(ulong x) => RotR(x, 28) ^ RotR(x, 34) ^ RotR(x, 39); + + private static ulong Sigma1(ulong x) => RotR(x, 14) ^ RotR(x, 18) ^ RotR(x, 41); + + private static ulong sigma0(ulong x) => RotR(x, 1) ^ RotR(x, 8) ^ (x >> 7); + + private static ulong sigma1(ulong x) => RotR(x, 19) ^ RotR(x, 61) ^ (x >> 6); + + private static ulong SwapEndianness(ulong x) => + ((x & 0x00000000000000FF) << 56) | + ((x & 0x000000000000FF00) << 40) | + ((x & 0x0000000000FF0000) << 24) | + ((x & 0x00000000FF000000) << 8) | + ((x & 0x000000FF00000000) >> 8) | + ((x & 0x0000FF0000000000) >> 24) | + ((x & 0x00FF000000000000) >> 40) | + ((x & 0xFF00000000000000) >> 56); +} diff --git a/NATS.Jwt/JsonContext.cs b/NATS.Jwt/JsonContext.cs new file mode 100644 index 0000000..6936cc2 --- /dev/null +++ b/NATS.Jwt/JsonContext.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; +using NATS.Jwt.Models; + +namespace NATS.Jwt; + +[JsonSerializable(typeof(JwtClaimsData))] +[JsonSerializable(typeof(JwtHeader))] +[JsonSerializable(typeof(JetStreamLimits))] +[JsonSerializable(typeof(NatsAccount))] +[JsonSerializable(typeof(NatsAccountClaims))] +[JsonSerializable(typeof(NatsExport))] +[JsonSerializable(typeof(NatsExternalAuthorization))] +[JsonSerializable(typeof(NatsGenericFields))] +[JsonSerializable(typeof(NatsImport))] +[JsonSerializable(typeof(NatsMsgTrace))] +[JsonSerializable(typeof(NatsOperator))] +[JsonSerializable(typeof(NatsOperatorClaims))] +[JsonSerializable(typeof(NatsOperatorLimits))] +[JsonSerializable(typeof(NatsPermission))] +[JsonSerializable(typeof(NatsPermissions))] +[JsonSerializable(typeof(NatsResponsePermission))] +[JsonSerializable(typeof(NatsServiceLatency))] +[JsonSerializable(typeof(NatsUser))] +[JsonSerializable(typeof(NatsUserClaims))] +[JsonSerializable(typeof(NatsWeightedMapping))] +[JsonSerializable(typeof(TimeRange))] +[JsonSerializable(typeof(NatsAuthorizationRequestClaims))] +[JsonSerializable(typeof(NatsAuthorizationRequest))] +[JsonSerializable(typeof(NatsServerId))] +[JsonSerializable(typeof(NatsClientInformation))] +[JsonSerializable(typeof(NatsConnectOptions))] +[JsonSerializable(typeof(NatsClientTls))] +[JsonSerializable(typeof(NatsGenericClaims))] +[JsonSerializable(typeof(NatsActivationClaims))] +[JsonSerializable(typeof(NatsActivation))] +[JsonSerializable(typeof(NatsAuthorizationResponseClaims))] +[JsonSerializable(typeof(NatsAuthorizationResponse))] +[JsonSerializable(typeof(NatsGenericFieldsClaims))] +public sealed partial class JsonContext : JsonSerializerContext +{ +} diff --git a/NATS.Jwt/Models/JetStreamLimits.cs b/NATS.Jwt/Models/JetStreamLimits.cs new file mode 100644 index 0000000..284b275 --- /dev/null +++ b/NATS.Jwt/Models/JetStreamLimits.cs @@ -0,0 +1,62 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record JetStreamLimits +{ + /// + /// Max number of bytes stored in memory across all streams. (0 means disabled) + /// + [JsonPropertyName("mem_storage")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MemoryStorage { get; set; } + + /// + /// Max number of bytes stored on disk across all streams. (0 means disabled) + /// + [JsonPropertyName("disk_storage")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long DiskStorage { get; set; } + + /// + /// Max number of streams + /// + [JsonPropertyName("streams")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Streams { get; set; } + + /// + /// Max number of consumers + /// + [JsonPropertyName("consumer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Consumer { get; set; } + + /// + /// Max ack pending of a Stream + /// + [JsonPropertyName("max_ack_pending")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MaxAckPending { get; set; } + + /// + /// Max bytes a memory backed stream can have. (0 means disabled/unlimited) + /// + [JsonPropertyName("mem_max_stream_bytes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MemoryMaxStreamBytes { get; set; } + + /// + /// Max bytes a disk backed stream can have. (0 means disabled/unlimited) + /// + [JsonPropertyName("disk_max_stream_bytes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long DiskMaxStreamBytes { get; set; } + + /// + /// Max bytes required by all Streams + /// + [JsonPropertyName("max_bytes_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool MaxBytesRequired { get; set; } +} diff --git a/NATS.Jwt/Models/JwtClaimsData.cs b/NATS.Jwt/Models/JwtClaimsData.cs new file mode 100644 index 0000000..1c47724 --- /dev/null +++ b/NATS.Jwt/Models/JwtClaimsData.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models +{ + public record JwtClaimsData + { + [JsonPropertyName("aud")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Audience { get; set; } + + [JsonPropertyName("jti")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Id { get; set; } + + [JsonPropertyName("iat")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long IssuedAt { get; set; } + + [JsonPropertyName("iss")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Issuer { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + [JsonPropertyName("sub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Subject { get; set; } + + [JsonPropertyName("exp")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Expires { get; set; } + + [JsonPropertyName("nbf")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long NotBefore { get; set; } + } +} diff --git a/NATS.Jwt/Models/JwtHeader.cs b/NATS.Jwt/Models/JwtHeader.cs new file mode 100644 index 0000000..de9986e --- /dev/null +++ b/NATS.Jwt/Models/JwtHeader.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record JwtHeader +{ + [JsonPropertyName("typ")] + public string Type { get; set; } + + [JsonPropertyName("alg")] + public string Algorithm { get; set; } +} diff --git a/NATS.Jwt/Models/NatsAccount.cs b/NATS.Jwt/Models/NatsAccount.cs new file mode 100644 index 0000000..f1f6d55 --- /dev/null +++ b/NATS.Jwt/Models/NatsAccount.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsAccount : NatsGenericFields +{ + [JsonPropertyName("imports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? Imports { get; set; } + + [JsonPropertyName("exports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List? Exports { get; set; } + + [JsonPropertyName("limits")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsOperatorLimits Limits { get; set; } = new(); + + [JsonPropertyName("signing_keys")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List SigningKeys { get; set; } + + [JsonPropertyName("revocations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Dictionary Revocations { get; set; } + + [JsonPropertyName("default_permissions")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsPermissions DefaultPermissions { get; set; } = new(); + + [JsonPropertyName("mappings")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Dictionary Mappings { get; set; } + + [JsonPropertyName("authorization")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsExternalAuthorization Authorization { get; set; } = new(); + + [JsonPropertyName("trace")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsMsgTrace Trace { get; set; } + + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Description { get; set; } + + [JsonPropertyName("info_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string InfoUrl { get; set; } +} diff --git a/NATS.Jwt/Models/NatsAccountClaims.cs b/NATS.Jwt/Models/NatsAccountClaims.cs new file mode 100644 index 0000000..44bbc82 --- /dev/null +++ b/NATS.Jwt/Models/NatsAccountClaims.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsAccountClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsAccount Account { get; set; } = new(); +} diff --git a/NATS.Jwt/Models/NatsActivationClaims.cs b/NATS.Jwt/Models/NatsActivationClaims.cs new file mode 100644 index 0000000..8f9eddf --- /dev/null +++ b/NATS.Jwt/Models/NatsActivationClaims.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsActivationClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsActivation Activation { get; set; } = new(); +} + +public record NatsActivation : NatsGenericFields +{ + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string ImportSubject { get; set; } + + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int ImportType { get; set; } + + /// + /// IssuerAccount stores the public key for the account the issuer represents. + /// When set, the claim was issued by a signing key. + /// + [JsonPropertyName("issuer_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string IssuerAccount { get; set; } +} diff --git a/NATS.Jwt/Models/NatsAuthorizationRequestClaims.cs b/NATS.Jwt/Models/NatsAuthorizationRequestClaims.cs new file mode 100644 index 0000000..a925022 --- /dev/null +++ b/NATS.Jwt/Models/NatsAuthorizationRequestClaims.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsAuthorizationRequestClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsAuthorizationRequest AuthorizationRequest { get; set; } = new(); +} + +public record NatsAuthorizationRequest : NatsGenericFields +{ + [JsonPropertyName("server_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public NatsServerId NatsServer { get; set; } = new(); + + [JsonPropertyName("user_nkey")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string UserNKey { get; set; } + + [JsonPropertyName("client_info")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public NatsClientInformation NatsClientInformation { get; set; } = new(); + + [JsonPropertyName("connect_opts")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public NatsConnectOptions NatsConnectOptions { get; set; } = new(); + + [JsonPropertyName("client_tls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsClientTls Tls { get; set; } + + [JsonPropertyName("request_nonce")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string RequestNonce { get; set; } +} + +public record NatsServerId +{ + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Host { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Id { get; set; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Version { get; set; } + + [JsonPropertyName("cluster")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Cluster { get; set; } + + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List Tags { get; set; } + + [JsonPropertyName("xkey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string XKey { get; set; } +} + +public record NatsClientInformation +{ + [JsonPropertyName("host")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Host { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Id { get; set; } + + [JsonPropertyName("user")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string User { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List Tags { get; set; } + + [JsonPropertyName("name_tag")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string NameTag { get; set; } + + [JsonPropertyName("kind")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Kind { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Type { get; set; } + + [JsonPropertyName("mqtt_id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Mqtt { get; set; } + + [JsonPropertyName("nonce")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Nonce { get; set; } +} + +public record NatsConnectOptions +{ + [JsonPropertyName("jwt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Jwt { get; set; } + + [JsonPropertyName("nkey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string NKey { get; set; } + + [JsonPropertyName("sig")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string SignedNonce { get; set; } + + [JsonPropertyName("auth_token")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Token { get; set; } + + [JsonPropertyName("user")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Username { get; set; } + + [JsonPropertyName("pass")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Password { get; set; } + + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + [JsonPropertyName("lang")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Lang { get; set; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Version { get; set; } + + [JsonPropertyName("protocol")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public int Protocol { get; set; } +} + +public record NatsClientTls +{ + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Version { get; set; } + + [JsonPropertyName("cipher")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Cipher { get; set; } + + [JsonPropertyName("certs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List Certs { get; set; } + + [JsonPropertyName("verified_chains")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List> VerifiedChains { get; set; } +} diff --git a/NATS.Jwt/Models/NatsAuthorizationResponseClaims.cs b/NATS.Jwt/Models/NatsAuthorizationResponseClaims.cs new file mode 100644 index 0000000..d2f25ed --- /dev/null +++ b/NATS.Jwt/Models/NatsAuthorizationResponseClaims.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsAuthorizationResponseClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsAuthorizationResponse AuthorizationResponse { get; set; } = new(); +} + +public record NatsAuthorizationResponse : NatsGenericFields +{ + [JsonPropertyName("jwt")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Jwt { get; set; } + + [JsonPropertyName("error")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Error { get; set; } + + /// + /// IssuerAccount stores the public key for the account the issuer represents. + /// When set, the claim was issued by a signing key. + /// + [JsonPropertyName("issuer_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string IssuerAccount { get; set; } +} diff --git a/NATS.Jwt/Models/NatsExport.cs b/NATS.Jwt/Models/NatsExport.cs new file mode 100644 index 0000000..071f98b --- /dev/null +++ b/NATS.Jwt/Models/NatsExport.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsExport +{ + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Subject { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Type { get; set; } + + [JsonPropertyName("token_req")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool TokenReq { get; set; } + + /// + /// Mapping of public keys to unix timestamps + /// + [JsonPropertyName("revocations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Dictionary Revocations { get; set; } + + [JsonPropertyName("response_type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string ResponseType { get; set; } + + [JsonPropertyName("response_threshold")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public TimeSpan ResponseThreshold { get; set; } + + [JsonPropertyName("service_latency")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsServiceLatency Latency { get; set; } + + [JsonPropertyName("account_token_position")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public uint AccountTokenPosition { get; set; } + + [JsonPropertyName("advertise")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Advertise { get; set; } + + [JsonPropertyName("allow_trace")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool AllowTrace { get; set; } + + [JsonPropertyName("description")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Description { get; set; } + + [JsonPropertyName("info_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string InfoUrl { get; set; } +} diff --git a/NATS.Jwt/Models/NatsExternalAuthorization.cs b/NATS.Jwt/Models/NatsExternalAuthorization.cs new file mode 100644 index 0000000..e9c8970 --- /dev/null +++ b/NATS.Jwt/Models/NatsExternalAuthorization.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsExternalAuthorization +{ + [JsonPropertyName("auth_users")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List AuthUsers { get; set; } + + [JsonPropertyName("allowed_accounts")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List AllowedAccounts { get; set; } + + [JsonPropertyName("xkey")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string XKey { get; set; } +} diff --git a/NATS.Jwt/Models/NatsGenericClaims.cs b/NATS.Jwt/Models/NatsGenericClaims.cs new file mode 100644 index 0000000..0d9da9c --- /dev/null +++ b/NATS.Jwt/Models/NatsGenericClaims.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsGenericClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public Dictionary Data { get; set; } = new(); +} diff --git a/NATS.Jwt/Models/NatsGenericFields.cs b/NATS.Jwt/Models/NatsGenericFields.cs new file mode 100644 index 0000000..661db37 --- /dev/null +++ b/NATS.Jwt/Models/NatsGenericFields.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsGenericFields +{ + [JsonPropertyName("tags")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List Tags { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Type { get; set; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Version { get; set; } +} diff --git a/NATS.Jwt/Models/NatsGenericFieldsClaims.cs b/NATS.Jwt/Models/NatsGenericFieldsClaims.cs new file mode 100644 index 0000000..0a160a5 --- /dev/null +++ b/NATS.Jwt/Models/NatsGenericFieldsClaims.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsGenericFieldsClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsGenericFields GenericFields { get; set; } = new(); +} diff --git a/NATS.Jwt/Models/NatsImport.cs b/NATS.Jwt/Models/NatsImport.cs new file mode 100644 index 0000000..d14fd11 --- /dev/null +++ b/NATS.Jwt/Models/NatsImport.cs @@ -0,0 +1,67 @@ +using System; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsImport +{ + /// + /// Name of the import. + /// + [JsonPropertyName("name")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Name { get; set; } + + /// + /// Subject field in an import is always from the perspective of the + /// initial publisher. In the case of a stream it is the account owning + /// the stream (the exporter), and in the case of a service it is the + /// account making the request (the importer). + /// + [JsonPropertyName("subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Subject { get; set; } + + [JsonPropertyName("account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Account { get; set; } + + [JsonPropertyName("token")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Token { get; set; } + + /// + /// To field in an import is always from the perspective of the subscriber. + /// In the case of a stream it is the client of the stream (the importer), + /// from the perspective of a service, it is the subscription waiting for + /// requests (the exporter). If the field is empty, it will default to the + /// value in the Subject field. + /// + [Obsolete("Use LocalSubject instead")] + [JsonPropertyName("to")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string To { get; set; } + + /// + /// Local subject used to subscribe (for streams) and publish (for services) to. + /// This value only needs setting if you want to change the value of the Subject. + /// If the value of Subject ends in > then LocalSubject needs to end in > as well. + /// LocalSubject can contain number wildcard references where number references the nth wildcard in Subject. + /// The sum of wildcard reference and * tokens needs to match the number of * tokens in Subject. + /// + [JsonPropertyName("local_subject")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string LocalSubject { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Type { get; set; } + + [JsonPropertyName("share")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool Share { get; set; } + + [JsonPropertyName("allow_trace")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool AllowTrace { get; set; } +} diff --git a/NATS.Jwt/Models/NatsMsgTrace.cs b/NATS.Jwt/Models/NatsMsgTrace.cs new file mode 100644 index 0000000..64006eb --- /dev/null +++ b/NATS.Jwt/Models/NatsMsgTrace.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsMsgTrace +{ + /// + /// Destination is the subject the server will send message traces to + /// if the inbound message contains the "traceparent" header and has + /// its sampled field indicating that the trace should be triggered. + /// + [JsonPropertyName("dest")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Destination { get; set; } + + /// + /// Sampling is used to set the probability sampling, that is, the + /// server will get a random number between 1 and 100 and trigger + /// the trace if the number is lower than this Sampling value. + /// The valid range is [1..100]. If the value is not set Validate() + /// will set the value to 100. + /// + [JsonPropertyName("sampling")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Sampling { get; set; } +} diff --git a/NATS.Jwt/Models/NatsOperator.cs b/NATS.Jwt/Models/NatsOperator.cs new file mode 100644 index 0000000..b8a9dc9 --- /dev/null +++ b/NATS.Jwt/Models/NatsOperator.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsOperator : NatsGenericFields +{ + /// + /// List of other operator NKeys that can be used to sign on behalf of the main + /// operator identity. + /// + [JsonPropertyName("signing_keys")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List SigningKeys { get; set; } + + /// + /// AccountServerURL is a partial URL like "https://host.domain.org:port/jwt/v1" + /// tools will use the prefix and build queries by appending /accounts/account_id + /// or /operator to the path provided. Note this assumes that the account server + /// can handle requests in a nats-account-server compatible way. See + /// https://github.com/nats-io/nats-account-server for an example. + /// + [JsonPropertyName("account_server_url")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string AccountServerURL { get; set; } + + /// + /// A list of NATS urls (tls://host:port) where tools can connect to the server + /// using proper credentials. + /// + [JsonPropertyName("operator_service_urls")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public List OperatorServiceURLs { get; set; } + + /// + /// Identity of the system account + /// + [JsonPropertyName("system_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string SystemAccount { get; set; } + + /// + /// Min Server version + /// + [JsonPropertyName("assert_server_version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string AssertServerVersion { get; set; } + + /// + /// Signing of subordinate objects will require signing keys + /// + [JsonPropertyName("strict_signing_key_usage")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool StrictSigningKeyUsage { get; set; } +} diff --git a/NATS.Jwt/Models/NatsOperatorClaims.cs b/NATS.Jwt/Models/NatsOperatorClaims.cs new file mode 100644 index 0000000..a46b9f7 --- /dev/null +++ b/NATS.Jwt/Models/NatsOperatorClaims.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsOperatorClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsOperator Operator { get; set; } = new(); +} diff --git a/NATS.Jwt/Models/NatsOperatorLimits.cs b/NATS.Jwt/Models/NatsOperatorLimits.cs new file mode 100644 index 0000000..31faa70 --- /dev/null +++ b/NATS.Jwt/Models/NatsOperatorLimits.cs @@ -0,0 +1,130 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsOperatorLimits +{ + /// + /// Max number of subscriptions + /// + [JsonPropertyName("subs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Subs { get; set; } = NatsJwt.NoLimit; + + /// + /// Max number of bytes + /// + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Data { get; set; } = NatsJwt.NoLimit; + + /// + /// Max message payload + /// + [JsonPropertyName("payload")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Payload { get; set; } = NatsJwt.NoLimit; + + /// + /// number of imports + /// + [JsonPropertyName("imports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Imports { get; set; } = NatsJwt.NoLimit; + + /// + /// Max number of exports + /// + [JsonPropertyName("exports")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Exports { get; set; } = NatsJwt.NoLimit; + + /// + /// Are wildcards allowed in exports + /// + [JsonPropertyName("wildcards")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool WildcardExports { get; set; } = true; + + /// + /// User JWT can't be bearer token + /// + [JsonPropertyName("disallow_bearer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool DisallowBearer { get; set; } + + /// + /// Max number of active connections + /// + [JsonPropertyName("conn")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Conn { get; set; } = NatsJwt.NoLimit; + + /// + /// Max number of active leaf node connections + /// + [JsonPropertyName("leaf")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long LeafNodeConn { get; set; } = NatsJwt.NoLimit; + + /// + /// Max number of bytes stored in memory across all streams. (0 means disabled) + /// + [JsonPropertyName("mem_storage")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MemoryStorage { get; set; } + + /// + /// Max number of bytes stored on disk across all streams. (0 means disabled) + /// + [JsonPropertyName("disk_storage")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long DiskStorage { get; set; } + + /// + /// Max number of streams + /// + [JsonPropertyName("streams")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Streams { get; set; } + + /// + /// Max number of consumers + /// + [JsonPropertyName("consumer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Consumer { get; set; } + + /// + /// Max ack pending of a Stream + /// + [JsonPropertyName("max_ack_pending")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MaxAckPending { get; set; } + + /// + /// Max bytes a memory backed stream can have. (0 means disabled/unlimited) + /// + [JsonPropertyName("mem_max_stream_bytes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long MemoryMaxStreamBytes { get; set; } + + /// + /// Max bytes a disk backed stream can have. (0 means disabled/unlimited) + /// + [JsonPropertyName("disk_max_stream_bytes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long DiskMaxStreamBytes { get; set; } + + /// + /// Max bytes required by all Streams + /// + [JsonPropertyName("max_bytes_required")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool MaxBytesRequired { get; set; } + + [JsonPropertyName("tiered_limits")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public Dictionary JetStreamTieredLimits { get; set; } +} diff --git a/NATS.Jwt/Models/NatsPermission.cs b/NATS.Jwt/Models/NatsPermission.cs new file mode 100644 index 0000000..e906d99 --- /dev/null +++ b/NATS.Jwt/Models/NatsPermission.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models +{ + public record NatsPermission + { + [JsonPropertyName("allow")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[] Allow { get; set; } + + [JsonPropertyName("deny")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[] Deny { get; set; } + } +} diff --git a/NATS.Jwt/Models/NatsPermissions.cs b/NATS.Jwt/Models/NatsPermissions.cs new file mode 100644 index 0000000..1cdb551 --- /dev/null +++ b/NATS.Jwt/Models/NatsPermissions.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsPermissions +{ + [JsonPropertyName("pub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsPermission Pub { get; set; } = new(); + + [JsonPropertyName("sub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsPermission Sub { get; set; } = new(); + + [JsonPropertyName("resp")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsResponsePermission Resp { get; set; } +} diff --git a/NATS.Jwt/Models/NatsResponsePermission.cs b/NATS.Jwt/Models/NatsResponsePermission.cs new file mode 100644 index 0000000..c677fc7 --- /dev/null +++ b/NATS.Jwt/Models/NatsResponsePermission.cs @@ -0,0 +1,18 @@ +using System; +using System.Text.Json.Serialization; +using NATS.Jwt.Internal; + +namespace NATS.Jwt.Models +{ + public record NatsResponsePermission + { + [JsonPropertyName("max")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int MaxMsgs { get; set; } + + [JsonPropertyName("ttl")] + [JsonConverter(typeof(NatsJsJsonNanosecondsConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public TimeSpan Expires { get; set; } + } +} diff --git a/NATS.Jwt/Models/NatsServiceLatency.cs b/NATS.Jwt/Models/NatsServiceLatency.cs new file mode 100644 index 0000000..8bd01b7 --- /dev/null +++ b/NATS.Jwt/Models/NatsServiceLatency.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsServiceLatency +{ + [JsonPropertyName("sampling")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int Sampling { get; set; } + + [JsonPropertyName("results")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Results { get; set; } +} diff --git a/NATS.Jwt/Models/NatsUser.cs b/NATS.Jwt/Models/NatsUser.cs new file mode 100644 index 0000000..42f8179 --- /dev/null +++ b/NATS.Jwt/Models/NatsUser.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsUser : NatsGenericFields +{ + [JsonPropertyName("pub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsPermission Pub { get; set; } = new(); + + [JsonPropertyName("sub")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsPermission Sub { get; set; } = new(); + + [JsonPropertyName("resp")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public NatsResponsePermission Resp { get; set; } + + [JsonPropertyName("src")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[] Src { get; set; } + + [JsonPropertyName("times")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public TimeRange[] Times { get; set; } + + [JsonPropertyName("times_location")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Locale { get; set; } + + [JsonPropertyName("subs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Subs { get; set; } = NatsJwt.NoLimit; + + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Data { get; set; } = NatsJwt.NoLimit; + + [JsonPropertyName("payload")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public long Payload { get; set; } = NatsJwt.NoLimit; + + [JsonPropertyName("bearer_token")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool BearerToken { get; set; } + + [JsonPropertyName("allowed_connection_types")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string[] AllowedConnectionTypes { get; set; } + + [JsonPropertyName("issuer_account")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string IssuerAccount { get; set; } +} diff --git a/NATS.Jwt/Models/NatsUserClaims.cs b/NATS.Jwt/Models/NatsUserClaims.cs new file mode 100644 index 0000000..1008b76 --- /dev/null +++ b/NATS.Jwt/Models/NatsUserClaims.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsUserClaims : JwtClaimsData +{ + [JsonPropertyName("nats")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + [JsonPropertyOrder(1)] + public NatsUser User { get; set; } = new(); +} diff --git a/NATS.Jwt/Models/NatsWeightedMapping.cs b/NATS.Jwt/Models/NatsWeightedMapping.cs new file mode 100644 index 0000000..2ad3480 --- /dev/null +++ b/NATS.Jwt/Models/NatsWeightedMapping.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models; + +public record NatsWeightedMapping +{ + [JsonPropertyName("subject")] + public string Subject { get; set; } + + [JsonPropertyName("weight")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public byte Weight { get; set; } + + [JsonPropertyName("cluster")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Cluster { get; set; } +} diff --git a/NATS.Jwt/Models/TimeRange.cs b/NATS.Jwt/Models/TimeRange.cs new file mode 100644 index 0000000..8d1d698 --- /dev/null +++ b/NATS.Jwt/Models/TimeRange.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace NATS.Jwt.Models +{ + public record TimeRange + { + [JsonPropertyName("start")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string Start { get; set; } + + [JsonPropertyName("end")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public string End { get; set; } + } +} diff --git a/NATS.Jwt/NATS.Jwt.csproj b/NATS.Jwt/NATS.Jwt.csproj new file mode 100644 index 0000000..bccd3ce --- /dev/null +++ b/NATS.Jwt/NATS.Jwt.csproj @@ -0,0 +1,38 @@ + + + netstandard2.0;net8.0 + latest + enable + true + true + + + nats;jwt + NATS JWT for .NET + true + + + + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/NATS.Jwt/NatsJwt.cs b/NATS.Jwt/NatsJwt.cs new file mode 100644 index 0000000..5074e78 --- /dev/null +++ b/NATS.Jwt/NatsJwt.cs @@ -0,0 +1,228 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using NATS.Jwt.Internal; +using NATS.Jwt.Models; +using NATS.NKeys; + +namespace NATS.Jwt; + +#pragma warning disable SA1303 +#pragma warning disable SA1202 +#pragma warning disable SA1204 + +public class NatsJwt +{ + public static readonly JwtHeader NatsJwtHeader = new() { Type = TokenTypeJwt, Algorithm = AlgorithmNkey }; + + private const int LibraryVersion = 2; + + public const long NoLimit = -1; + + public const string AnyAccount = "*"; + + // OperatorClaim is the type of an operator JWT + public const string OperatorClaim = "operator"; + + // AccountClaim is the type of an Account JWT + public const string AccountClaim = "account"; + + // UserClaim is the type of an user JWT + public const string UserClaim = "user"; + + // ActivationClaim is the type of an activation JWT + public const string ActivationClaim = "activation"; + + // AuthorizationRequestClaim is the type of an auth request claim JWT + public const string AuthorizationRequestClaim = "authorization_request"; + + // AuthorizationResponseClaim is the response for an auth request + public const string AuthorizationResponseClaim = "authorization_response"; + + // GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim + public const string GenericClaim = "generic"; + + // TokenTypeJwt is the JWT token type supported JWT tokens + // encoded and decoded by this library + // from RFC7519 5.1 "typ": + // it is RECOMMENDED that "JWT" always be spelled using uppercase characters for compatibility + public const string TokenTypeJwt = "JWT"; + + // AlgorithmNkey is the algorithm supported by JWT tokens + // encoded and decoded by this library + private const string AlgorithmNkeyOld = "ed25519"; + + public const string AlgorithmNkey = AlgorithmNkeyOld + "-nkey"; + + public string FormatUserConfig(string jwt, string seed) + { + // TODO: Decode JWT and validate + var parts = jwt.Split('.'); + var json = EncodingUtils.FromBase64UrlEncoded(parts[1]); + var fields = JsonSerializer.Deserialize(json, JsonContext.Default.NatsGenericFieldsClaims); + var type = fields!.GenericFields.Type; + if (type != UserClaim) + { + throw new NatsJwtException($"{type} can't be serialized as a user config"); + } + + var jwtKind = type.ToUpperInvariant(); + + var seedKind = seed.StartsWith("SU", StringComparison.Ordinal) ? "USER" + : seed.StartsWith("SA", StringComparison.Ordinal) ? "ACCOUNT" + : seed.StartsWith("SO", StringComparison.Ordinal) ? "OPERATOR" + : throw new NatsJwtException("Seed is not an operator, account or user seed"); + + return $""" + -----BEGIN NATS {jwtKind} JWT----- + {jwt} + ------END NATS {jwtKind} JWT------ + + ************************* IMPORTANT ************************* + NKEY Seed printed below can be used to sign and prove identity. + NKEYs are sensitive and should be treated as secrets. + + -----BEGIN {seedKind} NKEY SEED----- + {seed} + ------END {seedKind} NKEY SEED------ + + ************************************************************* + """; + } + + public NatsActivationClaims NewActivationClaims(string subject) => new() { Subject = subject }; + + public NatsAuthorizationRequestClaims NewAuthorizationRequestClaims(string subject) => new() { Subject = subject }; + + public NatsAuthorizationResponseClaims NewAuthorizationResponseClaims(string subject) => new() { Subject = subject }; + + public NatsGenericClaims NewGenericClaims(string subject) => new() { Subject = subject }; + + public NatsOperatorClaims NewOperatorClaims(string subject) => new() { Subject = subject, Issuer = subject }; + + public NatsUserClaims NewUserClaims(string subject) => new() { Subject = subject }; + + public NatsAccountClaims NewAccountClaims(string subject) => new() { Subject = subject }; + + /********************************************************************************************/ + + public string Encode(NatsActivationClaims activationClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(activationClaims.Activation, ActivationClaim); + return DoEncode(NatsJwtHeader, keyPair, activationClaims, JsonContext.Default.NatsActivationClaims, issuedAt); + } + + public string Encode(NatsAuthorizationRequestClaims authorizationRequestClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(authorizationRequestClaims.AuthorizationRequest, AuthorizationRequestClaim); + return DoEncode(NatsJwtHeader, keyPair, authorizationRequestClaims, JsonContext.Default.NatsAuthorizationRequestClaims, issuedAt); + } + + public string Encode(NatsAuthorizationResponseClaims authorizationResponseClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(authorizationResponseClaims.AuthorizationResponse, AuthorizationRequestClaim); + return DoEncode(NatsJwtHeader, keyPair, authorizationResponseClaims, JsonContext.Default.NatsAuthorizationResponseClaims, issuedAt); + } + + public string Encode(NatsGenericClaims genericClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + return DoEncode(NatsJwtHeader, keyPair, genericClaims, JsonContext.Default.NatsGenericClaims, issuedAt); + } + + public string Encode(NatsOperatorClaims operatorClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(operatorClaims.Operator, OperatorClaim); + return DoEncode(NatsJwtHeader, keyPair, operatorClaims, JsonContext.Default.NatsOperatorClaims, issuedAt); + } + + public string Encode(NatsUserClaims userClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(userClaims.User, UserClaim); + return DoEncode(NatsJwtHeader, keyPair, userClaims, JsonContext.Default.NatsUserClaims, issuedAt); + } + + public string Encode(NatsAccountClaims accountClaims, KeyPair keyPair, DateTimeOffset? issuedAt = null) + { + SetVersion(accountClaims.Account, AccountClaim); + accountClaims.Account.Imports?.Sort(); + accountClaims.Account.Exports?.Sort(); + return DoEncode(NatsJwtHeader, keyPair, accountClaims, JsonContext.Default.NatsAccountClaims, issuedAt); + } + + /********************************************************************************************/ + + private void SetVersion(T claims, string type) + where T : NatsGenericFields + { + claims.Type = type; + claims.Version = LibraryVersion; + } + + private string DoEncode(JwtHeader jwtHeader, KeyPair keyPair, T claim, JsonTypeInfo typeInfo, DateTimeOffset? now) + where T : JwtClaimsData + { + using var writer = new NatsBufferWriter(); + + var issuedAt = now ?? DateTimeOffset.UtcNow; + + var h = Serialize(jwtHeader, JsonContext.Default.JwtHeader); + var issuerBytes = keyPair.GetPublicKey(); + + // TODO: Validate prefixes + var c = claim; + + c.Issuer = issuerBytes; + c.IssuedAt = issuedAt.ToUnixTimeSeconds(); + + // TODO: ID generation same as Go implementation + // c.Id = Hash(c, typeInfo); + c.Id = Hash(c, JsonContext.Default.JwtClaimsData); + + var payload = Serialize(c, typeInfo); + + // TODO: Check algorithm, only allow ed25519 + var toSign = $"{h}.{payload}"; + var sig = Encoding.ASCII.GetBytes(toSign); + var signature = new byte[64]; + keyPair.Sign(sig, signature); + var eSig = EncodingUtils.ToBase64UrlEncoded(signature); + + return $"{toSign}.{eSig}"; + } + + private string Hash(T c, JsonTypeInfo typeInfo) + { + using var writer = new NatsBufferWriter(); + var jsonWriter = new Utf8JsonWriter(writer); + JsonSerializer.Serialize(jsonWriter, c, typeInfo); + var bytes = writer.WrittenMemory.ToArray(); + + // TODO: ID generation same as Go implementation + // It's just an ID so we can use SHA-256 + // var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + // hasher.AppendData(bytes); + // var hashResult = hasher.GetHashAndReset(); + var hashResult = Sha512256.ComputeHash(bytes); + + Span hashResultChars = stackalloc char[Base32.GetEncodedLength(hashResult)]; + Base32.ToBase32(hashResult, hashResultChars); + return hashResultChars.ToString(); + } + + private static string Serialize(T data, JsonTypeInfo typeInfo) + { + using var writer = new NatsBufferWriter(); + var jsonWriter = new Utf8JsonWriter(writer); + JsonSerializer.Serialize(jsonWriter, data, typeInfo); +#if DEBUG + var bytes = writer.WrittenMemory.ToArray(); + var json = Encoding.UTF8.GetString(bytes); +#endif + return EncodingUtils.ToBase64UrlEncoded(writer.WrittenMemory.ToArray()); + } +} + +public class NatsJwtException(string message) : Exception(message); diff --git a/README.md b/README.md new file mode 100644 index 0000000..2738e7f --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# NATS JWT .NET + +[![codecov](https://codecov.io/github/nats-io/jwt.net/graph/badge.svg?token=zXUTHG6L3Q)](https://codecov.io/github/nats-io/jwt.net) + +**IMPORTANT**: Under development at the moment, not ready for use. + +This is a .NET implementation of the JWT library for the NATS ecosystem. + +## Installation + +You can install the package via NuGet: + +```bash +dotnet add package NATS.Jwt --prerelease +``` + +## Usage + +```csharp +var jwt = new NatsJwt(); + +// create an operator key pair (private key) +var okp = KeyPair.CreatePair(PrefixByte.Operator); +var opk = okp.GetPublicKey(); + +// create an operator claim using the public key for the identifier +var oc = jwt.NewOperatorClaims(opk); +oc.Name = "Example Operator"; + +// add an operator signing key to sign accounts +var oskp = KeyPair.CreatePair(PrefixByte.Operator); +var ospk = oskp.GetPublicKey(); + +// add the signing key to the operator - this makes any account +// issued by the signing key to be valid for the operator +oc.Operator.SigningKeys = [ospk]; + +// self-sign the operator JWT - the operator trusts itself +var operatorJwt = jwt.Encode(oc, okp); + +// create an account keypair +var akp = KeyPair.CreatePair(PrefixByte.Account); +var apk = akp.GetPublicKey(); + +// create the claim for the account using the public key of the account +var ac = jwt.NewAccountClaims(apk); +ac.Name = "Example Account"; + +var askp = KeyPair.CreatePair(PrefixByte.Account); +var aspk = askp.GetPublicKey(); + +// add the signing key (public) to the account +ac.Account.SigningKeys = [aspk]; +var accountJwt = jwt.Encode(ac, oskp); + +// now back to the account, the account can issue users +// need not be known to the operator - the users are trusted +// because they will be signed by the account. The server will +// look up the account get a list of keys the account has and +// verify that the user was issued by one of those keys +var ukp = KeyPair.CreatePair(PrefixByte.User); +var upk = ukp.GetPublicKey(); +var uc = jwt.NewUserClaims(upk); + +// since the jwt will be issued by a signing key, the issuer account +// must be set to the public ID of the account +uc.User.IssuerAccount = apk; +var userJwt = jwt.Encode(uc, askp); + +// the seed is a version of the keypair that is stored as text +var userSeed = ukp.GetSeed(); + +var conf = $$""" + operator: {{operatorJwt}} + + resolver: MEMORY + resolver_preload: { + {{apk}}: {{accountJwt}} + } + """; + +// generate a creds formatted file that can be used by a NATS client +const string credsPath = $"example_user.creds"; +File.WriteAllText(credsPath, jwt.FormatUserConfig(userJwt, userSeed)); + +// now we are going to put it together into something that can be run +// we create a file to store the server configuration, the creds +// file and a small program that uses the creds file +const string confPath = $"example_server.conf"; +File.WriteAllText(confPath, conf); + +// run the server: +// > nats-server -c example_server.conf + +// Connect as user +var authOpts = new NatsAuthOpts { CredsFile = credsPath }; +var opts = new NatsOpts { Url = server.Url, AuthOpts = authOpts }; +await using var nats = new NatsConnection(opts); +await nats.PingAsync(); +``` + +## About + +A [JWT](https://jwt.io/) implementation that uses [nkeys](https://github.com/nats-io/nkeys.net) to digitally sign +JWT tokens for the [NATS](https://nats.io/) ecosystem. + +See also https://github.com/nats-io/jwt diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..1d1679d --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.0-preview.0