pax_global_header00006660000000000000000000000064147270625310014521gustar00rootroot0000000000000052 comment=a768c381b3ed3c634bfa6738ba34a4652456986d jwt-2.7.3/000077500000000000000000000000001472706253100123365ustar00rootroot00000000000000jwt-2.7.3/.github/000077500000000000000000000000001472706253100136765ustar00rootroot00000000000000jwt-2.7.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001472706253100160615ustar00rootroot00000000000000jwt-2.7.3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000005051472706253100200510ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Discussion url: https://github.com/nats-io/jwt/discussions about: Ideal for ideas, feedback, or longer form questions. - name: Chat url: https://slack.nats.io about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! jwt-2.7.3/.github/ISSUE_TEMPLATE/defect.yml000066400000000000000000000024441472706253100200420ustar00rootroot00000000000000--- name: Defect description: Report a defect, such as a bug or regression. labels: - defect body: - type: textarea id: versions attributes: label: What version were you using? description: Include the server version (`nats-server --version`) and any client versions when observing the issue. validations: required: true - type: textarea id: environment attributes: label: What environment was the server running in? description: This pertains to the operating system, CPU architecture, and/or Docker image that was used. validations: required: true - type: textarea id: steps attributes: label: Is this defect reproducible? description: Provide best-effort steps to showcase the defect. validations: required: true - type: textarea id: expected attributes: label: Given the capability you are leveraging, describe your expectation? description: This may be the expected behavior or performance characteristics. validations: required: true - type: textarea id: actual attributes: label: Given the expectation, what is the defect you are observing? description: This may be an unexpected behavior or regression in performance. validations: required: true jwt-2.7.3/.github/ISSUE_TEMPLATE/proposal.yml000066400000000000000000000016671472706253100204550ustar00rootroot00000000000000--- name: Proposal description: Propose an enhancement or new feature. labels: - proposal body: - type: textarea id: usecase attributes: label: What motivated this proposal? description: Describe the use case justifying this request. validations: required: true - type: textarea id: change attributes: label: What is the proposed change? description: This could be a behavior change, enhanced API, or a branch new feature. validations: required: true - type: textarea id: benefits attributes: label: Who benefits from this change? description: Describe how this not only benefits you. validations: required: false - type: textarea id: alternates attributes: label: What alternatives have you evaluated? description: This could be using existing features or relying on an external dependency. validations: required: false jwt-2.7.3/.github/workflows/000077500000000000000000000000001472706253100157335ustar00rootroot00000000000000jwt-2.7.3/.github/workflows/go-test.yaml000066400000000000000000000045451472706253100202110ustar00rootroot00000000000000name: jwt testing on: [push, pull_request] jobs: test: name: ${{ matrix.config.kind }} ${{ matrix.config.os }} strategy: matrix: include: - go: stable os: ubuntu-latest canonical: true - go: stable os: windows-latest canonical: false env: GO111MODULE: "on" runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 1 - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{matrix.go}} - name: Install deps shell: bash --noprofile --norc -x -eo pipefail {0} run: | go install honnef.co/go/tools/cmd/staticcheck@latest go install github.com/client9/misspell/cmd/misspell@latest go install github.com/wadey/gocovmerge@latest - name: Lint shell: bash --noprofile --norc -x -eo pipefail {0} run: | cd v2 GO_LIST=$(go list ./...) go build $(exit $(go fmt $GO_LIST | wc -l)) go vet $GO_LIST which misspell find . -type f -name "*.go" | xargs misspell -error -locale US staticcheck $GO_LIST if: matrix.canonical - name: Tests shell: bash --noprofile --norc -x -eo pipefail {0} run: | set -e mkdir -p cov cd v2 go get -t ./... go test -v -race -covermode=atomic -coverprofile=../cov/v2.out -coverpkg=github.com/nats-io/jwt/v2 . cd v1compat go test -v -race -covermode=atomic -coverprofile=../../cov/v1.out -coverpkg=github.com/nats-io/jwt/v2/v1compat . cd ../.. gocovmerge ./cov/*.out > ./coverage.out set +e if: matrix.canonical - name: Tests (Windows) shell: bash --noprofile --norc -x -eo pipefail {0} run: | set -e cd v2 go get -t ./... go test -v -race . set +e if: runner.os == 'Windows' - name: Coverage uses: shogo82148/actions-goveralls@v1 with: # this needs to be where it can find a go.mod working-directory: ./v2 # this path-to-profile is relative to the working-directory path-to-profile: ../coverage.out if: matrix.canonical jwt-2.7.3/.gitignore000066400000000000000000000003511472706253100143250ustar00rootroot00000000000000# Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # IDE Files .vscode .idea/ coverage.outjwt-2.7.3/LICENSE000066400000000000000000000261351472706253100133520ustar00rootroot00000000000000 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. jwt-2.7.3/README.md000066400000000000000000000126731472706253100136260ustar00rootroot00000000000000# JWT A [JWT](https://jwt.io/) implementation that uses [nkeys](https://github.com/nats-io/nkeys) to digitally sign JWT tokens. Nkeys use [Ed25519](https://ed25519.cr.yp.to/) to provide authentication of JWT claims. [![License Apache 2](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![ReportCard](https://goreportcard.com/badge/github.com/nats-io/jwt)](https://goreportcard.com/report/nats-io/jwt) [![Build Status](https://travis-ci.com/nats-io/jwt.svg?branch=master)](https://travis-ci.com/github/nats-io/jwt) [![GoDoc](https://godoc.org/github.com/nats-io/jwt?status.png)](https://godoc.org/github.com/nats-io/jwt) [![Coverage Status](https://coveralls.io/repos/github/nats-io/jwt/badge.svg?branch=main)](https://coveralls.io/github/nats-io/jwt?branch=main) ```go // create an operator key pair (private key) okp, err := nkeys.CreateOperator() if err != nil { t.Fatal(err) } // extract the public key opk, err := okp.PublicKey() if err != nil { t.Fatal(err) } // create an operator claim using the public key for the identifier oc := jwt.NewOperatorClaims(opk) oc.Name = "O" // add an operator signing key to sign accounts oskp, err := nkeys.CreateOperator() if err != nil { t.Fatal(err) } // get the public key for the signing key ospk, err := oskp.PublicKey() if err != nil { t.Fatal(err) } // add the signing key to the operator - this makes any account // issued by the signing key to be valid for the operator oc.SigningKeys.Add(ospk) // self-sign the operator JWT - the operator trusts itself operatorJWT, err := oc.Encode(okp) if err != nil { t.Fatal(err) } // create an account keypair akp, err := nkeys.CreateAccount() if err != nil { t.Fatal(err) } // extract the public key for the account apk, err := akp.PublicKey() if err != nil { t.Fatal(err) } // create the claim for the account using the public key of the account ac := jwt.NewAccountClaims(apk) ac.Name = "A" // create a signing key that we can use for issuing users askp, err := nkeys.CreateAccount() if err != nil { t.Fatal(err) } // extract the public key aspk, err := askp.PublicKey() if err != nil { t.Fatal(err) } // add the signing key (public) to the account ac.SigningKeys.Add(aspk) // now we could encode an issue the account using the operator // key that we generated above, but this will illustrate that // the account could be self-signed, and given to the operator // who can then re-sign it accountJWT, err := ac.Encode(akp) if err != nil { t.Fatal(err) } // the operator would decode the provided token, if the token // is not self-signed or signed by an operator or tampered with // the decoding would fail ac, err = jwt.DecodeAccountClaims(accountJWT) if err != nil { t.Fatal(err) } // here the operator is going to use its private signing key to // re-issue the account accountJWT, err = ac.Encode(oskp) if err != nil { t.Fatal(err) } // 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 ukp, err := nkeys.CreateUser() if err != nil { t.Fatal(err) } upk, err := ukp.PublicKey() if err != nil { t.Fatal(err) } 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.IssuerAccount = apk userJwt, err := uc.Encode(askp) if err != nil { t.Fatal(err) } // the seed is a version of the keypair that is stored as text useed, err := ukp.Seed() if err != nil { t.Fatal(err) } // generate a creds formatted file that can be used by a NATS client creds, err := jwt.FormatUserConfig(userJwt, useed) if err != nil { t.Fatal(err) } // now we are going to put it together into something that can be run // we create a directory to store the server configuration, the creds // file and a small go program that uses the creds file dir, err := os.MkdirTemp(os.TempDir(), "jwt_example") if err != nil { t.Fatal(err) } // print where we generated the file t.Logf("generated example %s", dir) t.Log("to run this example:") t.Logf("> cd %s", dir) t.Log("> go mod init example") t.Log("> go mod tidy") t.Logf("> nats-server -c %s/resolver.conf &", dir) t.Log("> go run main.go") // we are generating a memory resolver server configuration // it lists the operator and all account jwts the server should // know about resolver := fmt.Sprintf(`operator: %s resolver: MEMORY resolver_preload: { %s: %s } `, operatorJWT, apk, accountJWT) if err := os.WriteFile(path.Join(dir, "resolver.conf"), []byte(resolver), 0644); err != nil { t.Fatal(err) } // store the creds credsPath := path.Join(dir, "u.creds") if err := os.WriteFile(credsPath, creds, 0644); err != nil { t.Fatal(err) } // here we generate as small go program that connects using the creds file // subscribes, and publishes a message connect := fmt.Sprintf(` package main import ( "fmt" "sync" "github.com/nats-io/nats.go" ) func main() { var wg sync.WaitGroup wg.Add(1) nc, err := nats.Connect(nats.DefaultURL, nats.UserCredentials(%q)) if err != nil { panic(err) } nc.Subscribe("hello.world", func(m *nats.Msg) { fmt.Println(m.Subject) wg.Done() }) nc.Publish("hello.world", []byte("hello")) nc.Flush() wg.Wait() nc.Close() } `, credsPath) if err := os.WriteFile(path.Join(dir, "main.go"), []byte(connect), 0644); err != nil { t.Fatal(err) } ```jwt-2.7.3/ReleaseNotes.md000066400000000000000000000002011472706253100152420ustar00rootroot00000000000000# Release Notes ## 0.3.0 * Removed revocation claims in favor of timestamp-based revocation maps in account and export claims. jwt-2.7.3/dependencies.md000066400000000000000000000003261472706253100153070ustar00rootroot00000000000000# External Dependencies This file lists the dependencies used in this repository. | Dependency | License | |-|-| | github.com/nats-io/nkeys | Apache License 2.0 | | go | BSD 3-Clause "New" or "Revised" License | jwt-2.7.3/scripts/000077500000000000000000000000001472706253100140255ustar00rootroot00000000000000jwt-2.7.3/scripts/cov.sh000077500000000000000000000007711472706253100151600ustar00rootroot00000000000000#!/bin/bash -e # Run from directory above via ./scripts/cov.sh rm -rf ./cov mkdir cov go test -v -race -covermode=atomic -coverprofile=./cov/coverage.out -coverpkg=github.com/nats-io/jwt . gocovmerge ./cov/*.out > coverage.out rm -rf ./cov # If we have an arg, assume travis run and push to coveralls. Otherwise launch browser results if [[ -n $1 ]]; then $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service travis-ci rm -rf ./coverage.out else go tool cover -html=coverage.out fijwt-2.7.3/scripts/test.sh000077500000000000000000000007301472706253100153430ustar00rootroot00000000000000#!/bin/bash -e # Run from directory above via ./scripts/test.sh gofmt -s -w *.go goimports -w *.go go vet ./... go test -v go test -v --race cd v2 && ( gofmt -s -w *.go goimports -w *.go go vet -modfile=go_test.mod ./... go test github.com/nats-io/jwt/v2 -v go test github.com/nats-io/jwt/v2 -v --race go test -modfile=go_test.mod github.com/nats-io/jwt/v2/test -v go test -modfile=go_test.mod github.com/nats-io/jwt/v2/test -v --race ) staticcheck ./... jwt-2.7.3/v2/000077500000000000000000000000001472706253100126655ustar00rootroot00000000000000jwt-2.7.3/v2/Makefile000066400000000000000000000003651472706253100143310ustar00rootroot00000000000000.PHONY: test cover build: go build fmt: gofmt -w -s *.go goimports -w *.go go mod tidy test: go vet ./... staticcheck ./... rm -rf ./coverage.out go test -v -coverprofile=./coverage.out ./... cover: go tool cover -html=coverage.outjwt-2.7.3/v2/account_claims.go000066400000000000000000000406331472706253100162060ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "sort" "time" "github.com/nats-io/nkeys" ) // NoLimit is used to indicate a limit field is unlimited in value. const ( NoLimit = -1 AnyAccount = "*" ) type AccountLimits struct { Imports int64 `json:"imports,omitempty"` // Max number of imports Exports int64 `json:"exports,omitempty"` // Max number of exports WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports DisallowBearer bool `json:"disallow_bearer,omitempty"` // User JWT can't be bearer token Conn int64 `json:"conn,omitempty"` // Max number of active connections LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections } // IsUnlimited returns true if all limits are unlimited func (a *AccountLimits) IsUnlimited() bool { return *a == AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit} } type NatsLimits struct { Subs int64 `json:"subs,omitempty"` // Max number of subscriptions Data int64 `json:"data,omitempty"` // Max number of bytes Payload int64 `json:"payload,omitempty"` // Max message payload } // IsUnlimited returns true if all limits are unlimited func (n *NatsLimits) IsUnlimited() bool { return *n == NatsLimits{NoLimit, NoLimit, NoLimit} } type JetStreamLimits struct { MemoryStorage int64 `json:"mem_storage,omitempty"` // Max number of bytes stored in memory across all streams. (0 means disabled) DiskStorage int64 `json:"disk_storage,omitempty"` // Max number of bytes stored on disk across all streams. (0 means disabled) Streams int64 `json:"streams,omitempty"` // Max number of streams Consumer int64 `json:"consumer,omitempty"` // Max number of consumers MaxAckPending int64 `json:"max_ack_pending,omitempty"` // Max ack pending of a Stream MemoryMaxStreamBytes int64 `json:"mem_max_stream_bytes,omitempty"` // Max bytes a memory backed stream can have. (0 means disabled/unlimited) DiskMaxStreamBytes int64 `json:"disk_max_stream_bytes,omitempty"` // Max bytes a disk backed stream can have. (0 means disabled/unlimited) MaxBytesRequired bool `json:"max_bytes_required,omitempty"` // Max bytes required by all Streams } // IsUnlimited returns true if all limits are unlimited func (j *JetStreamLimits) IsUnlimited() bool { lim := *j // workaround in case NoLimit was used instead of 0 if lim.MemoryMaxStreamBytes < 0 { lim.MemoryMaxStreamBytes = 0 } if lim.DiskMaxStreamBytes < 0 { lim.DiskMaxStreamBytes = 0 } if lim.MaxAckPending < 0 { lim.MaxAckPending = 0 } return lim == JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit, 0, 0, 0, false} } type JetStreamTieredLimits map[string]JetStreamLimits // OperatorLimits are used to limit access by an account type OperatorLimits struct { NatsLimits AccountLimits JetStreamLimits JetStreamTieredLimits `json:"tiered_limits,omitempty"` } // IsJSEnabled returns if this account claim has JS enabled either through a tier or the non tiered limits. func (o *OperatorLimits) IsJSEnabled() bool { if len(o.JetStreamTieredLimits) > 0 { for _, l := range o.JetStreamTieredLimits { if l.MemoryStorage != 0 || l.DiskStorage != 0 { return true } } return false } l := o.JetStreamLimits return l.MemoryStorage != 0 || l.DiskStorage != 0 } // IsEmpty returns true if all limits are 0/false/empty. func (o *OperatorLimits) IsEmpty() bool { return o.NatsLimits == NatsLimits{} && o.AccountLimits == AccountLimits{} && o.JetStreamLimits == JetStreamLimits{} && len(o.JetStreamTieredLimits) == 0 } // IsUnlimited returns true if all limits are unlimited func (o *OperatorLimits) IsUnlimited() bool { return o.AccountLimits.IsUnlimited() && o.NatsLimits.IsUnlimited() && o.JetStreamLimits.IsUnlimited() && len(o.JetStreamTieredLimits) == 0 } // Validate checks that the operator limits contain valid values func (o *OperatorLimits) Validate(vr *ValidationResults) { // negative values mean unlimited, so all numbers are valid if len(o.JetStreamTieredLimits) > 0 { if (o.JetStreamLimits != JetStreamLimits{}) { vr.AddError("JetStream Limits and tiered JetStream Limits are mutually exclusive") } if _, ok := o.JetStreamTieredLimits[""]; ok { vr.AddError(`Tiered JetStream Limits can not contain a blank "" tier name`) } } } // WeightedMapping for publishes type WeightedMapping struct { Subject Subject `json:"subject"` Weight uint8 `json:"weight,omitempty"` Cluster string `json:"cluster,omitempty"` } func (m *WeightedMapping) GetWeight() uint8 { if m.Weight == 0 { return 100 } return m.Weight } type Mapping map[Subject][]WeightedMapping func (m *Mapping) Validate(vr *ValidationResults) { for ubFrom, wm := range (map[Subject][]WeightedMapping)(*m) { ubFrom.Validate(vr) perCluster := make(map[string]uint8) total := uint8(0) for _, e := range wm { e.Subject.Validate(vr) if e.Cluster != "" { t := perCluster[e.Cluster] t += e.Weight perCluster[e.Cluster] = t if t > 100 { vr.AddError("Mapping %q in cluster %q exceeds 100%% among all of it's weighted to mappings", ubFrom, e.Cluster) } } else { total += e.GetWeight() } } if total > 100 { vr.AddError("Mapping %q exceeds 100%% among all of it's weighted to mappings", ubFrom) } } } func (a *Account) AddMapping(sub Subject, to ...WeightedMapping) { a.Mappings[sub] = to } // ExternalAuthorization enables external authorization for account users. // AuthUsers are those users specified to bypass the authorization callout and should be used for the authorization service itself. // AllowedAccounts specifies which accounts, if any, that the authorization service can bind an authorized user to. // The authorization response, a user JWT, will still need to be signed by the correct account. // If optional XKey is specified, that is the public xkey (x25519) and the server will encrypt the request such that only the // holder of the private key can decrypt. The auth service can also optionally encrypt the response back to the server using it's // public xkey which will be in the authorization request. type ExternalAuthorization struct { AuthUsers StringList `json:"auth_users,omitempty"` AllowedAccounts StringList `json:"allowed_accounts,omitempty"` XKey string `json:"xkey,omitempty"` } func (ac *ExternalAuthorization) IsEnabled() bool { return len(ac.AuthUsers) > 0 } // HasExternalAuthorization helper function to determine if external authorization is enabled. func (a *Account) HasExternalAuthorization() bool { return a.Authorization.IsEnabled() } // EnableExternalAuthorization helper function to setup external authorization. func (a *Account) EnableExternalAuthorization(users ...string) { a.Authorization.AuthUsers.Add(users...) } func (ac *ExternalAuthorization) Validate(vr *ValidationResults) { if len(ac.AllowedAccounts) > 0 && len(ac.AuthUsers) == 0 { vr.AddError("External authorization cannot have accounts without users specified") } // Make sure users are all valid user nkeys. // Make sure allowed accounts are all valid account nkeys. for _, u := range ac.AuthUsers { if !nkeys.IsValidPublicUserKey(u) { vr.AddError("AuthUser %q is not a valid user public key", u) } } for _, a := range ac.AllowedAccounts { if a == AnyAccount && len(ac.AllowedAccounts) > 1 { vr.AddError("AllowedAccounts can only be a list of accounts or %q", AnyAccount) continue } else if a == AnyAccount { continue } else if !nkeys.IsValidPublicAccountKey(a) { vr.AddError("Account %q is not a valid account public key", a) } } if ac.XKey != "" && !nkeys.IsValidPublicCurveKey(ac.XKey) { vr.AddError("XKey %q is not a valid public xkey", ac.XKey) } } const ( ClusterTrafficSystem = "system" ClusterTrafficOwner = "owner" ) type ClusterTraffic string func (ct ClusterTraffic) Valid() error { if ct == "" || ct == ClusterTrafficSystem || ct == ClusterTrafficOwner { return nil } return fmt.Errorf("unknown cluster traffic option: %q", ct) } // Account holds account specific claims data type Account struct { Imports Imports `json:"imports,omitempty"` Exports Exports `json:"exports,omitempty"` Limits OperatorLimits `json:"limits,omitempty"` SigningKeys SigningKeys `json:"signing_keys,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` DefaultPermissions Permissions `json:"default_permissions,omitempty"` Mappings Mapping `json:"mappings,omitempty"` Authorization ExternalAuthorization `json:"authorization,omitempty"` Trace *MsgTrace `json:"trace,omitempty"` ClusterTraffic ClusterTraffic `json:"cluster_traffic,omitempty"` Info GenericFields } // MsgTrace holds distributed message tracing configuration type MsgTrace struct { // 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. Destination Subject `json:"dest,omitempty"` // 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. Sampling int `json:"sampling,omitempty"` } // Validate checks if the account is valid, based on the wrapper func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) { a.Imports.Validate(acct.Subject, vr) a.Exports.Validate(vr) a.Limits.Validate(vr) a.DefaultPermissions.Validate(vr) a.Mappings.Validate(vr) a.Authorization.Validate(vr) if a.Trace != nil { tvr := CreateValidationResults() a.Trace.Destination.Validate(tvr) if !tvr.IsEmpty() { vr.AddError(fmt.Sprintf("the account Trace.Destination %s", tvr.Issues[0].Description)) } if a.Trace.Destination.HasWildCards() { vr.AddError("the account Trace.Destination subject %q is not a valid publish subject", a.Trace.Destination) } if a.Trace.Sampling < 0 || a.Trace.Sampling > 100 { vr.AddError("the account Trace.Sampling value '%d' is not valid, should be in the range [1..100]", a.Trace.Sampling) } else if a.Trace.Sampling == 0 { a.Trace.Sampling = 100 } } if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports { vr.AddError("the account contains more imports than allowed by the operator") } // Check Imports and Exports for limit violations. if a.Limits.Imports != NoLimit { if int64(len(a.Imports)) > a.Limits.Imports { vr.AddError("the account contains more imports than allowed by the operator") } } if a.Limits.Exports != NoLimit { if int64(len(a.Exports)) > a.Limits.Exports { vr.AddError("the account contains more exports than allowed by the operator") } // Check for wildcard restrictions if !a.Limits.WildcardExports { for _, ex := range a.Exports { if ex.Subject.HasWildCards() { vr.AddError("the account contains wildcard exports that are not allowed by the operator") } } } } a.SigningKeys.Validate(vr) a.Info.Validate(vr) if err := a.ClusterTraffic.Valid(); err != nil { vr.AddError(err.Error()) } } // AccountClaims defines the body of an account JWT type AccountClaims struct { ClaimsData Account `json:"nats,omitempty"` } // NewAccountClaims creates a new account JWT func NewAccountClaims(subject string) *AccountClaims { if subject == "" { return nil } c := &AccountClaims{} c.SigningKeys = make(SigningKeys) // Set to unlimited to start. We do it this way so we get compiler // errors if we add to the OperatorLimits. c.Limits = OperatorLimits{ NatsLimits{NoLimit, NoLimit, NoLimit}, AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit}, JetStreamLimits{0, 0, 0, 0, 0, 0, 0, false}, JetStreamTieredLimits{}, } c.Subject = subject c.Mappings = Mapping{} return c } // Encode converts account claims into a JWT string func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) { return a.EncodeWithSigner(pair, nil) } func (a *AccountClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicAccountKey(a.Subject) { return "", errors.New("expected subject to be account public key") } sort.Sort(a.Exports) sort.Sort(a.Imports) a.Type = AccountClaim return a.ClaimsData.encode(pair, a, fn) } // DecodeAccountClaims decodes account claims from a JWT string func DecodeAccountClaims(token string) (*AccountClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } ac, ok := claims.(*AccountClaims) if !ok { return nil, errors.New("not account claim") } return ac, nil } func (a *AccountClaims) String() string { return a.ClaimsData.String(a) } // Payload pulls the accounts specific payload out of the claims func (a *AccountClaims) Payload() interface{} { return &a.Account } // Validate checks the accounts contents func (a *AccountClaims) Validate(vr *ValidationResults) { a.ClaimsData.Validate(vr) a.Account.Validate(a, vr) if nkeys.IsValidPublicAccountKey(a.ClaimsData.Issuer) { if !a.Limits.IsEmpty() { vr.AddWarning("self-signed account JWTs shouldn't contain operator limits") } } } func (a *AccountClaims) ClaimType() ClaimType { return a.Type } func (a *AccountClaims) updateVersion() { a.GenericFields.Version = libVersion } // ExpectedPrefixes defines the types that can encode an account jwt, account and operator func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} } // Claims returns the accounts claims data func (a *AccountClaims) Claims() *ClaimsData { return &a.ClaimsData } func (a *AccountClaims) GetTags() TagList { return a.Account.Tags } // DidSign checks the claims against the account's public key and its signing keys func (a *AccountClaims) DidSign(c Claims) bool { if c != nil { issuer := c.Claims().Issuer if issuer == a.Subject { return true } uc, ok := c.(*UserClaims) if ok && uc.IssuerAccount == a.Subject { return a.SigningKeys.Contains(issuer) } at, ok := c.(*ActivationClaims) if ok && at.IssuerAccount == a.Subject { return a.SigningKeys.Contains(issuer) } } return false } // Revoke enters a revocation by public key using time.Now(). func (a *AccountClaims) Revoke(pubKey string) { a.RevokeAt(pubKey, time.Now()) } // RevokeAt enters a revocation by public key and timestamp into this account // This will revoke all jwt issued for pubKey, prior to timestamp // If there is already a revocation for this public key that is newer, it is kept. // The value is expected to be a public key or "*" (means all public keys) func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) { if a.Revocations == nil { a.Revocations = RevocationList{} } a.Revocations.Revoke(pubKey, timestamp) } // ClearRevocation removes any revocation for the public key func (a *AccountClaims) ClearRevocation(pubKey string) { a.Revocations.ClearRevocation(pubKey) } // isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in. // Generally this method is called with the subject and issue time of the jwt to be tested. // DO NOT pass time.Now(), it will not produce a stable/expected response. func (a *AccountClaims) isRevoked(pubKey string, claimIssuedAt time.Time) bool { return a.Revocations.IsRevoked(pubKey, claimIssuedAt) } // IsClaimRevoked checks if the account revoked the claim passed in. // Invalid claims (nil, no Subject or IssuedAt) will return true. func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool { if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { return true } return a.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) } jwt-2.7.3/v2/account_claims_test.go000066400000000000000000000705641472706253100172530ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "fmt" "strings" "testing" "time" "github.com/nats-io/nkeys" ) func TestNewAccountClaims(t *testing.T) { akp := createAccountNKey(t) akp2 := createAccountNKey(t) apk := publicKey(akp, t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream actJWT := encode(activation, akp2, t) account := NewAccountClaims(apk) if !account.Limits.NatsLimits.IsUnlimited() { t.Fatalf("Expected unlimited nats operator limits") } if !account.Limits.AccountLimits.IsUnlimited() { t.Fatalf("Expected unlimited account operator limits") } if account.Limits.JetStreamLimits.DiskStorage != 0 || account.Limits.JetStreamLimits.MemoryStorage != 0 || account.Limits.JetStreamLimits.Consumer != 0 || account.Limits.JetStreamLimits.Streams != 0 { t.Fatalf("Expected unlimited operator limits") } account.Expires = time.Now().Add(time.Hour * 24 * 365).UTC().Unix() account.InfoURL = "http://localhost/my-account/doc" account.Description = "my account" account.Imports = Imports{} account.Imports.Add(&Import{Subject: "test", Name: "test import", Account: apk2, Token: actJWT, LocalSubject: "my", Type: Stream}) vr := CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Valid account will have no validation results") } actJwt := encode(account, akp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) AssertEquals(account2.IsSelfSigned(), true, t) AssertEquals(account2.Claims() != nil, true, t) AssertEquals(account2.Payload() != nil, true, t) AssertEquals(account.InfoURL, account2.InfoURL, t) AssertEquals(account.Description, account2.Description, t) } func TestAccountCanSignOperatorLimits(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Hour * 24 * 365).Unix() account.Limits.Conn = 10 account.Limits.LeafNodeConn = 2 _, err := account.Encode(akp) if err != nil { t.Fatal("account should not be able to encode operator limits", err) } } func TestOperatorCanSignClaims(t *testing.T) { akp := createAccountNKey(t) okp := createOperatorNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Hour * 24 * 365).Unix() account.Limits.Conn = 1 account.Limits.LeafNodeConn = 4 actJwt := encode(account, okp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) AssertEquals(account2.IsSelfSigned(), false, t) if account2.Limits.Conn != 1 { t.Fatalf("Expected Limits.Conn == 1, got %d", account2.Limits.Conn) } if account2.Limits.LeafNodeConn != 4 { t.Fatalf("Expected Limits.Conn == 4, got %d", account2.Limits.LeafNodeConn) } } func TestInvalidAccountClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ac := NewAccountClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Hour).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeAccountClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestInvalidAccountSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { pk := publicKey(i.kp, t) var err error c := NewAccountClaims(pk) _, err = c.Encode(i.kp) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode account with with %q subject", i.name) t.Fail() } } } func TestAccountImports(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Hour * 24 * 365).Unix() actJwt := encode(account, akp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) } func TestNewNilAccountClaim(t *testing.T) { v := NewAccountClaims("") if v != nil { t.Fatal("expected nil account claim") } } func TestLimitValidationInAccount(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Hour * 24 * 365).Unix() account.Limits.Conn = 10 account.Limits.Imports = 10 account.Limits.Exports = 10 account.Limits.Data = 1024 account.Limits.Payload = 1024 account.Limits.Subs = 10 account.Limits.WildcardExports = true vr := CreateValidationResults() account.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid account should have no validation issues") } account.Limits.Conn = -1 account.Limits.Imports = -1 account.Limits.Exports = -1 account.Limits.Subs = -1 account.Limits.Data = -1 account.Limits.Payload = -1 vr = CreateValidationResults() account.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid account should have no validation issues") } op := createOperatorNKey(t) opk := publicKey(op, t) account.Issuer = opk vr = CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("operator can encode limits and identity") } account.Issuer = apk vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("bad issuer for limits should have non-blocking validation results") } } func TestWildcardExportLimit(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Hour * 24 * 365).Unix() account.Limits.Conn = 10 account.Limits.Imports = 10 account.Limits.Exports = 10 account.Limits.WildcardExports = true account.Exports = Exports{ &Export{Subject: "foo", Type: Stream}, &Export{Subject: "bar.*", Type: Stream}, } vr := CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() { t.Fatal("valid account should have no validation issues") } account.Limits.WildcardExports = false vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Fatal("invalid account should have validation issues") } account.Limits.WildcardExports = true account.Limits.Exports = 1 vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Fatal("invalid account should have validation issues") } } func TestJetstreamLimits(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) acc1 := NewAccountClaims(apk) if acc1.Limits.JetStreamLimits.DiskStorage != 0 || acc1.Limits.JetStreamLimits.MemoryStorage != 0 || acc1.Limits.JetStreamLimits.Consumer != 0 || acc1.Limits.JetStreamLimits.Streams != 0 || acc1.Limits.JetStreamLimits.MaxBytesRequired != false || acc1.Limits.JetStreamLimits.MemoryMaxStreamBytes != 0 || acc1.Limits.JetStreamLimits.DiskMaxStreamBytes != 0 || acc1.Limits.JetStreamLimits.MaxAckPending != 0 { t.Fatalf("Expected unlimited operator limits") } acc1.Limits.Consumer = 1 acc1.Limits.Streams = 2 acc1.Limits.MemoryStorage = 3 acc1.Limits.DiskStorage = 4 acc1.Limits.MemoryMaxStreamBytes = 1000 acc1.Limits.DiskMaxStreamBytes = 1000 acc1.Limits.MaxBytesRequired = true acc1.Limits.MaxAckPending = 200 vr := CreateValidationResults() acc1.Validate(vr) if !vr.IsEmpty() { t.Fatal("valid account should have no validation issues") } if token, err := acc1.Encode(akp); err != nil { t.Fatal("valid account should have no validation issues") } else if acc2, err := DecodeAccountClaims(token); err != nil { t.Fatal("valid account should have no validation issues") } else if acc1.Limits.JetStreamLimits != acc2.Limits.JetStreamLimits { t.Fatal("account should have same properties") } } func TestTieredLimits(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) acc1 := NewAccountClaims(apk) l := JetStreamLimits{ MemoryStorage: 1024, DiskStorage: 1024, Streams: 1, Consumer: 1, MaxBytesRequired: true, } acc1.Limits.JetStreamTieredLimits["R1"] = l l.Streams = 2 // minor change so both tiers differ acc1.Limits.JetStreamTieredLimits["R3"] = l vr := CreateValidationResults() acc1.Validate(vr) if !vr.IsEmpty() { t.Fatal("valid account should have validation issues") } if token, err := acc1.Encode(akp); err != nil { t.Fatal("valid account should have no validation issues") } else if acc2, err := DecodeAccountClaims(token); err != nil { t.Fatal("valid account should have no validation issues") } else if acc1.Limits.JetStreamTieredLimits["R1"] != acc2.Limits.JetStreamTieredLimits["R1"] { t.Fatal("tier R1 should have same limits") } else if acc1.Limits.JetStreamTieredLimits["R3"] != acc2.Limits.JetStreamTieredLimits["R3"] { t.Fatal("tier R2 should have same limits") } // test tiers and js limits being mutual exclusive acc1.Limits.JetStreamLimits = l acc1.Validate(vr) if vr.IsEmpty() { t.Fatal("valid account should have validation issues") } } func TestJetstreamLimitsDeEnCode(t *testing.T) { // token (generated without this change) with js disabled c1, err := DecodeAccountClaims(`eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiI3V0NYQkZKS0lIN1Y0SkdPR0FVTEtTS05aQUxVTUZMVExZWVgyNTdDSzZORk5OWTdGRVdBIiwiaWF0IjoxNjQ0Mjc5NDY3LCJpc3MiOiJPQk5UUVJFSEVJUFJFVE1BVlBWUVVDSUdFUktHWkIzRVJBVjVTNUdNM0lPRVFOSFJFQkpPVUFSRiIsIm5hbWUiOiJ0ZXN0Iiwic3ViIjoiQUM2SFFJMlVBTVVQREVQN1dTWVFZV1JDTEVZQkxZVlNQTDZBSExDVVBKVEdVMzJUNEtRQktZU0ciLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwiZGVmYXVsdF9wZXJtaXNzaW9ucyI6eyJwdWIiOnt9LCJzdWIiOnt9fSwidHlwZSI6ImFjY291bnQiLCJ2ZXJzaW9uIjoyfX0.73vDu9osNeLKwQ-g1Uu1fAtszMNz_QRZXRJLp0kzZh-0eMBmt0nb2mMwBwg-fufJs1FqYe9WbWKeXAYStnVnBg`) if err != nil { t.Fatal(err) } else if c1.Limits.IsJSEnabled() { t.Fatal("JetStream expected to be disabled") } // token (generated without this change) with js enabled c2, err := DecodeAccountClaims(`eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJPVFFXVEQyVkFMWkRQQTZYU1hLS09GRVFZR1VaVFBDNEtKV1BYMlAyWU1XMjVTMzRTVjNRIiwiaWF0IjoxNjQ0Mjc5MzM0LCJpc3MiOiJPQk5UUVJFSEVJUFJFVE1BVlBWUVVDSUdFUktHWkIzRVJBVjVTNUdNM0lPRVFOSFJFQkpPVUFSRiIsIm5hbWUiOiJ0ZXN0Iiwic3ViIjoiQUM2SFFJMlVBTVVQREVQN1dTWVFZV1JDTEVZQkxZVlNQTDZBSExDVVBKVEdVMzJUNEtRQktZU0ciLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xLCJkaXNrX3N0b3JhZ2UiOjEwMDAwMDB9LCJkZWZhdWx0X3Blcm1pc3Npb25zIjp7InB1YiI6e30sInN1YiI6e319LCJ0eXBlIjoiYWNjb3VudCIsInZlcnNpb24iOjJ9fQ.Xt5azhxOkC7nywz9Q8xVtzX8lZIqdOhpfGyQI30aNdd-nbVGX2O13OOfouIaTLyajZiS4bcJFXa29q6QCFRUDA`) if err != nil { t.Fatal(err) } else if !c2.Limits.IsJSEnabled() { t.Fatal("JetStream expected to be enabled") } } func TestAccountSigningKeyValidation(t *testing.T) { okp := createOperatorNKey(t) akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2) var vr ValidationResults ac.Validate(&vr) if len(vr.Issues) != 0 { t.Fatal("expected no validation issues") } // try encoding/decoding token, err := ac.Encode(okp) if err != nil { t.Fatal(err) } ac2, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } if len(ac2.SigningKeys) != 1 { t.Fatal("expected claim to have a signing key") } if !ac.SigningKeys.Contains(apk2) { t.Fatalf("expected signing key %s", apk2) } bkp := createUserNKey(t) ac.SigningKeys.Add(publicKey(bkp, t)) ac.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected 1 validation issue") } } func TestAccountSignedBy(t *testing.T) { okp := createOperatorNKey(t) akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2) token, err := ac.Encode(okp) if err != nil { t.Fatal(err) } ac2, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } if len(ac2.SigningKeys) != 1 { t.Fatal("expected claim to have a signing key") } if !ac.SigningKeys.Contains(apk2) { t.Fatalf("expected signing key %s", apk2) } ukp := createUserNKey(t) upk := publicKey(ukp, t) // claim signed by alternate key uc := NewUserClaims(upk) uc.IssuerAccount = apk1 utoken, err := uc.Encode(akp2) if err != nil { t.Fatal(err) } uc2, err := DecodeUserClaims(utoken) if err != nil { t.Fatal(err) } if !ac2.DidSign(uc2) { t.Fatal("failed to verify user claim") } // claim signed by the account pk uc3 := NewUserClaims(upk) utoken2, err := uc3.Encode(akp1) if err != nil { t.Fatal(err) } uc4, err := DecodeUserClaims(utoken2) if err != nil { t.Fatal(err) } if !ac2.DidSign(uc4) { t.Fatal("failed to verify user claim") } } func TestAddRemoveSigningKey(t *testing.T) { akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) akp3 := createAccountNKey(t) apk3 := publicKey(akp3, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2, apk3) if len(ac.SigningKeys) != 2 { t.Fatal("expected 2 signing keys") } ac.SigningKeys.Remove(publicKey(createAccountNKey(t), t)) if len(ac.SigningKeys) != 2 { t.Fatal("expected 2 signing keys") } ac.SigningKeys.Remove(apk2) if len(ac.SigningKeys) != 1 { t.Fatal("expected single signing keys") } } func TestUserRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) ukp := createUserNKey(t) pubKey := publicKey(ukp, t) uc := NewUserClaims(pubKey) uJwt, _ := uc.Encode(akp) uc, err := DecodeUserClaims(uJwt) if err != nil { t.Errorf("Failed to decode user claim: %v", err) } now := time.Now() // test that clear is safe before we add any account.ClearRevocation(pubKey) if account.isRevoked(pubKey, now) { t.Errorf("no revocation was added so is revoked should be false") } account.RevokeAt(pubKey, now.Add(time.Second*100)) if !account.isRevoked(pubKey, now) { t.Errorf("revocation should hold when timestamp is in the future") } if account.isRevoked(pubKey, now.Add(time.Second*150)) { t.Errorf("revocation should time out") } account.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in if !account.isRevoked(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should hold, 100 > 50") } encoded, _ := account.Encode(akp) decoded, _ := DecodeAccountClaims(encoded) if !decoded.isRevoked(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should last across encoding") } account.ClearRevocation(pubKey) if account.IsClaimRevoked(uc) { t.Errorf("revocations should be cleared") } account.RevokeAt(pubKey, now.Add(time.Second*1000)) if !account.IsClaimRevoked(uc) { t.Errorf("revocation be true we revoked in the future") } } func TestAccountDefaultPermissions(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.DefaultPermissions.Sub = Permission{ Allow: []string{"foo.1", "bar.*"}, Deny: []string{"foo.2", "baz.>"}, } account.DefaultPermissions.Pub = Permission{ Allow: []string{"foo.4", "bar.>"}, Deny: []string{"foo.4", "baz.*"}, } account.DefaultPermissions.Resp = &ResponsePermission{ 5, 5 * time.Second} actJwt := encode(account, akp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) } func TestUserRevocationAll(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) upk := publicKey(ukp, t) user := NewUserClaims(upk) token, err := user.Encode(akp) if err != nil { t.Fatal(err) } ud, err := DecodeUserClaims(token) if err != nil { t.Fatal(err) } apk := publicKey(akp, t) account := NewAccountClaims(apk) account.RevokeAt(All, time.Now().Add(time.Second)) if !account.IsClaimRevoked(ud) { t.Fatal("user should have been revoked") } account.RevokeAt(All, time.Now().Add(time.Second*-10)) if !account.IsClaimRevoked(ud) { t.Fatal("user should have not been revoked") } } func TestInvalidAccountInfo(t *testing.T) { a := NewAccountClaims(publicKey(createAccountNKey(t), t)) a.InfoURL = "/bad" vr := CreateValidationResults() a.Validate(vr) if vr.IsEmpty() { t.Errorf("export info should not validate cleanly") } if !vr.IsBlocking(true) { t.Errorf("invalid info needs to be blocking") } } func TestAccountMapping(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) vr := &ValidationResults{} account.AddMapping("foo1", WeightedMapping{Subject: "to"}) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } account.AddMapping("foo2", WeightedMapping{Subject: "to1", Weight: 50}, WeightedMapping{Subject: "to2", Weight: 50}) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } account.AddMapping("foo3", WeightedMapping{Subject: "to1", Weight: 50}, WeightedMapping{Subject: "to2", Weight: 51}) account.Validate(vr) if !vr.IsBlocking(false) { t.Fatal("Expected blocking error as sum of weights is > 100") } vr = &ValidationResults{} account.Mappings = Mapping{} account.AddMapping("foo4", WeightedMapping{Subject: "to1"}, // no weight means 100 WeightedMapping{Subject: "to2", Weight: 1}) account.Validate(vr) if !vr.IsBlocking(false) { t.Fatal("Expected blocking error as sum of weights is > 100") } vr = &ValidationResults{} account.Mappings = Mapping{} account.AddMapping("foo5.>", WeightedMapping{Subject: "to.*.>"}) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } vr = &ValidationResults{} account.Mappings = Mapping{} account.AddMapping("foo6.>", WeightedMapping{Subject: "to.boo.>"}) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } } func TestAccountClusterMany100MappingOK(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) vr := &ValidationResults{} account.AddMapping("q", WeightedMapping{Subject: "qq", Weight: 100, Cluster: "A"}, WeightedMapping{Subject: "qq", Weight: 100, Cluster: "B"}, WeightedMapping{Subject: "bb", Weight: 100, Cluster: "C"}, WeightedMapping{Subject: "qq", Weight: 100}) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } } func TestAccountClusterNoOver100Mapping(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) vr := &ValidationResults{} account.AddMapping("q", WeightedMapping{Subject: "qq", Weight: 100, Cluster: "A"}, WeightedMapping{Subject: "qq", Weight: 5, Cluster: "A"}) account.Validate(vr) if !vr.IsBlocking(false) { t.Fatal("Expected blocking error as sum of weights is > 100") } } func TestAccountExternalAuthorization(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) vr := &ValidationResults{} ukp := createUserNKey(t) account.EnableExternalAuthorization(publicKey(ukp, t)) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } akp2 := createAccountNKey(t) account.Authorization.AllowedAccounts.Add(publicKey(akp2, t)) account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Expected no errors") } if !account.HasExternalAuthorization() { t.Fatalf("Expected to have authorization enabled") } vr = &ValidationResults{} account.Authorization = ExternalAuthorization{} account.Authorization.AuthUsers.Add("Z") account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatalf("Expected blocking error on bad auth user") } vr = &ValidationResults{} account.Authorization = ExternalAuthorization{} account.Authorization.AllowedAccounts.Add("Z") account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatalf("Expected blocking error on bad allowed account") } account.Authorization = ExternalAuthorization{} if account.Authorization.IsEnabled() { t.Fatalf("Expected not to have authorization enabled") } } func TestAccountExternalAuthorizationRequiresOneUser(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Authorization.AllowedAccounts.Add(publicKey(createAccountNKey(t), t)) vr := &ValidationResults{} account.Validate(vr) AssertEquals(len(vr.Errors()), 1, t) AssertEquals("External authorization cannot have accounts without users specified", vr.Errors()[0].Error(), t) } func TestAccountExternalAuthorizationAnyAccount(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) ukp := createUserNKey(t) upk := publicKey(ukp, t) account := NewAccountClaims(apk) account.Authorization.AllowedAccounts.Add("*") account.Authorization.AuthUsers.Add(upk) vr := &ValidationResults{} account.Validate(vr) AssertEquals(len(vr.Errors()), 0, t) } func TestAccountExternalAuthorizationAnyAccountAndSpecificFails(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) ukp := createUserNKey(t) upk := publicKey(ukp, t) account := NewAccountClaims(apk) account.Authorization.AllowedAccounts.Add("*", apk) account.Authorization.AuthUsers.Add(upk) vr := &ValidationResults{} account.Validate(vr) AssertEquals(len(vr.Errors()), 1, t) AssertEquals(vr.Errors()[0].Error(), fmt.Sprintf("AllowedAccounts can only be a list of accounts or %q", AnyAccount), t) } func TestAccountClaims_DidSign(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) skp := createAccountNKey(t) spk := publicKey(skp, t) ac := NewAccountClaims(apk) ac.SigningKeys.Add(spk) upk := publicKey(createUserNKey(t), t) uc := NewUserClaims(upk) tok, err := uc.Encode(akp) if err != nil { t.Fatal("error encoding") } uc, err = DecodeUserClaims(tok) if err != nil { t.Fatal("error decoding") } if !ac.DidSign(uc) { t.Fatal("expected account to have been issued") } uc = NewUserClaims(upk) uc.IssuerAccount = publicKey(createAccountNKey(t), t) tok, err = uc.Encode(skp) if err != nil { t.Fatalf("encode failed %v", err) } uc, err = DecodeUserClaims(tok) if err != nil { t.Fatalf("decode failed %v", err) } if ac.DidSign(uc) { t.Fatal("this is not issued by account A") } } func TestAccountClaims_GetTags(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) ac := NewAccountClaims(apk) ac.Account.Tags.Add("foo", "bar") tags := ac.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } token, err := ac.Encode(akp) if err != nil { t.Fatal("error encoding") } ac, err = DecodeAccountClaims(token) if err != nil { t.Fatal("error decoding") } tags = ac.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } } func TestAccountClaimsTraceDest(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) for i, test := range []struct { name string invalidSubj Subject expectErr bool }{ {"trace not specified", "", false}, {"trace created but with empty destination", "", true}, {"trace dest has spaces", "invalid dest", true}, {"trace dest start with a dot", ".invalid.dest", true}, {"trace dest ends with a dot", "invalid.dest.", true}, {"trace dest has consecutive dots", "invalid..dest", true}, {"trace dest invalid publish dest", "invalid.publish.*.dest", true}, } { t.Run(test.name, func(t *testing.T) { if i > 0 { account.Trace = &MsgTrace{Destination: test.invalidSubj} } vr := CreateValidationResults() account.Validate(vr) if test.expectErr && vr.IsEmpty() { t.Fatal("account validation should have failed") } else if !test.expectErr && !vr.IsEmpty() { t.Fatalf("account validation should not have failed, got %+v", vr.Issues) } }) } } func TestAccountClaimsTraceDestSampling(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) for _, test := range []struct { name string dest string sampling int expectErr string }{ {"sampling without destination", "", 10, "subject cannot be empty"}, {"sampling negative", "dest", -1, "should be in the range [1..100]"}, {"sampling above 100", "dest", 101, "should be in the range [1..100]"}, {"sampling at 50", "dest", 50, ""}, {"sampling at zero sets to 100", "dest", 0, ""}, } { t.Run(test.name, func(t *testing.T) { account.Trace = &MsgTrace{Destination: Subject(test.dest), Sampling: test.sampling} vr := CreateValidationResults() account.Validate(vr) if test.expectErr == "" { if !vr.IsEmpty() { t.Fatalf("account validation should not have failed, got %+v", vr.Issues) } if test.sampling == 0 { if account.Trace.Sampling != 100 { t.Fatalf("account sampling should have been set to 100 got %d", account.Trace.Sampling) } } else if test.sampling != account.Trace.Sampling { t.Fatalf("account sampling should be %d, got %d", test.sampling, account.Trace.Sampling) } } else { if vr.IsEmpty() { t.Fatal("account validation should have failed") } if !strings.Contains(vr.Issues[0].Description, test.expectErr) { t.Fatalf("account validation should have failed with error %q, got %q", test.expectErr, vr.Issues[0].Description) } } }) } } func TestClusterTraffic_Valid(t *testing.T) { type clustertest struct { input string ok bool } tests := []clustertest{ {input: "", ok: true}, {input: "system", ok: true}, {input: "SYSTEM", ok: false}, {input: "owner", ok: true}, {input: "OWNER", ok: false}, {input: "unknown", ok: false}, {input: "account", ok: false}, } for _, test := range tests { ct := ClusterTraffic(test.input) err := ct.Valid() if test.ok && err != nil { t.Fatalf("unexpected err for input %q: %v", test.input, err) } if !test.ok && err == nil { t.Fatalf("expected to fail input %q", test.input) } } } func TestSignFn(t *testing.T) { okp := createOperatorNKey(t) opub := publicKey(okp, t) opk, err := nkeys.FromPublicKey(opub) if err != nil { t.Fatal(err) } akp := createAccountNKey(t) pub := publicKey(akp, t) var ok bool ac := NewAccountClaims(pub) ac.Name = "A" s, err := ac.EncodeWithSigner(opk, func(pub string, data []byte) ([]byte, error) { if pub != opub { t.Fatal("expected pub key in callback to match") } ok = true return okp.Sign(data) }) if err != nil { t.Fatal("error encoding") } if !ok { t.Fatal("expected ok to be true") } ac, err = DecodeAccountClaims(s) if err != nil { t.Fatal("error decoding encoded jwt") } vr := CreateValidationResults() ac.Validate(vr) if !vr.IsEmpty() { t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) } } jwt-2.7.3/v2/activation_claims.go000066400000000000000000000120631472706253100167070ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "crypto/sha256" "encoding/base32" "errors" "fmt" "strings" "github.com/nats-io/nkeys" ) // Activation defines the custom parts of an activation claim type Activation struct { ImportSubject Subject `json:"subject,omitempty"` ImportType ExportType `json:"kind,omitempty"` // IssuerAccount stores the public key for the account the issuer represents. // When set, the claim was issued by a signing key. IssuerAccount string `json:"issuer_account,omitempty"` GenericFields } // IsService returns true if an Activation is for a service func (a *Activation) IsService() bool { return a.ImportType == Service } // IsStream returns true if an Activation is for a stream func (a *Activation) IsStream() bool { return a.ImportType == Stream } // Validate checks the exports and limits in an activation JWT func (a *Activation) Validate(vr *ValidationResults) { if !a.IsService() && !a.IsStream() { vr.AddError("invalid import type: %q", a.ImportType) } a.ImportSubject.Validate(vr) } // ActivationClaims holds the data specific to an activation JWT type ActivationClaims struct { ClaimsData Activation `json:"nats,omitempty"` } // NewActivationClaims creates a new activation claim with the provided sub func NewActivationClaims(subject string) *ActivationClaims { if subject == "" { return nil } ac := &ActivationClaims{} ac.Subject = subject return ac } // Encode turns an activation claim into a JWT strimg func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) { return a.EncodeWithSigner(pair, nil) } func (a *ActivationClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) { return "", errors.New("expected subject to be an account") } a.Type = ActivationClaim return a.ClaimsData.encode(pair, a, fn) } // DecodeActivationClaims tries to create an activation claim from a JWT string func DecodeActivationClaims(token string) (*ActivationClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } ac, ok := claims.(*ActivationClaims) if !ok { return nil, errors.New("not activation claim") } return ac, nil } // Payload returns the activation specific part of the JWT func (a *ActivationClaims) Payload() interface{} { return a.Activation } // Validate checks the claims func (a *ActivationClaims) Validate(vr *ValidationResults) { a.validateWithTimeChecks(vr, true) } // Validate checks the claims func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) { if timeChecks { a.ClaimsData.Validate(vr) } a.Activation.Validate(vr) if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) { vr.AddError("account_id is not an account public key") } } func (a *ActivationClaims) ClaimType() ClaimType { return a.Type } func (a *ActivationClaims) updateVersion() { a.GenericFields.Version = libVersion } // ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} } // Claims returns the generic part of the JWT func (a *ActivationClaims) Claims() *ClaimsData { return &a.ClaimsData } func (a *ActivationClaims) String() string { return a.ClaimsData.String(a) } // HashID returns a hash of the claims that can be used to identify it. // The hash is calculated by creating a string with // issuerPubKey.subjectPubKey. and constructing the sha-256 hash and base32 encoding that. // is the exported subject, minus any wildcards, so foo.* becomes foo. // the one special case is that if the export start with "*" or is ">" the "_" func (a *ActivationClaims) HashID() (string, error) { if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" { return "", fmt.Errorf("not enough data in the activaion claims to create a hash") } subject := cleanSubject(string(a.ImportSubject)) base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject) h := sha256.New() h.Write([]byte(base)) sha := h.Sum(nil) hash := base32.StdEncoding.EncodeToString(sha) return hash, nil } func cleanSubject(subject string) string { split := strings.Split(subject, ".") cleaned := "" for i, tok := range split { if tok == "*" || tok == ">" { if i == 0 { cleaned = "_" break } cleaned = strings.Join(split[:i], ".") break } } if cleaned == "" { cleaned = subject } return cleaned } jwt-2.7.3/v2/activation_claims_test.go000066400000000000000000000243661472706253100177570ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewActivationClaims(t *testing.T) { okp := createOperatorNKey(t) akp := createAccountNKey(t) apk := publicKey(akp, t) activation := NewActivationClaims(apk) activation.Expires = time.Now().Add(time.Hour).Unix() activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } actJwt := encode(activation, okp, t) activation2, err := DecodeActivationClaims(actJwt) if err != nil { t.Fatal("failed to decode activation", err) } AssertEquals(activation.String(), activation2.String(), t) AssertEquals(activation.Claims() != nil, true, t) AssertEquals(activation.Payload() != nil, true, t) } func TestInvalidActivationTargets(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewActivationClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode user with with %q subject", i.name) t.Fail() } } } func TestInvalidActivationClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ac := NewActivationClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Hour).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeActivationClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestPublicIsNotValid(t *testing.T) { c := NewActivationClaims("public") _, err := c.Encode(createOperatorNKey(t)) if err == nil { t.Fatal("should not have encoded public activation anymore") } } func TestNilActivationClaim(t *testing.T) { v := NewActivationClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestActivationImportSubjectValidation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportType = Service vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "foo.*" // wildcards are ok vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() { t.Error("wildcard service activation should pass validation") } activation.ImportSubject = ">" // wildcards are ok vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() { t.Error("wildcard service activation should pass validation") } activation.ImportType = Stream // Stream is ok with wildcards vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "" // empty strings are bad vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Error("empty activation should not pass validation") } activation.ImportSubject = "foo bar" // spaces are bad vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Error("spaces in activation should not pass validation") } } func TestActivationValidation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 activation.Expires = time.Now().Add(time.Hour).Unix() activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "times.*" activation.ImportType = Stream activation.Name = "times" vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } } func TestActivationHashIDLimits(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 _, err := activation.HashID() if err == nil { t.Fatal("activation without subject should fail to hash") } activation.ImportSubject = "times.*" activation.ImportType = Stream activation.Name = "times" hash, err := activation.HashID() if err != nil { t.Fatalf("activation with subject should hash %v", err) } activation2 := NewActivationClaims(apk) activation2.Issuer = apk activation2.Subject = apk2 activation2.ImportSubject = "times.*.bar" activation2.ImportType = Stream activation2.Name = "times" hash2, err := activation2.HashID() if err != nil { t.Fatalf("activation with subject should hash %v", err) } if hash != hash2 { t.Fatal("subjects should be stripped to create hash") } } func TestActivationClaimAccountIDValidation(t *testing.T) { issuerAccountKP := createAccountNKey(t) issuerAccountPK := publicKey(issuerAccountKP, t) issuerKP := createAccountNKey(t) issuerPK := publicKey(issuerKP, t) account := NewAccountClaims(issuerAccountPK) account.SigningKeys.Add(issuerPK) token, err := account.Encode(issuerAccountKP) if err != nil { t.Fatal(err) } account, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } importerKP := createAccountNKey(t) importerPK := publicKey(importerKP, t) ac := NewActivationClaims(importerPK) ac.IssuerAccount = issuerAccountPK ac.Name = "foo.bar" ac.Activation.ImportSubject = "foo.bar" ac.Activation.ImportType = Stream var vr ValidationResults ac.Validate(&vr) if len(vr.Issues) != 0 { t.Fatalf("expected no validation errors: %v", vr.Issues[0].Error()) } token, err = ac.Encode(issuerKP) if err != nil { t.Fatal(err) } ac, err = DecodeActivationClaims(token) if err != nil { t.Fatal(err) } if ac.Issuer != issuerPK { t.Fatal("expected activation subject to be different") } if ac.IssuerAccount != issuerAccountPK { t.Fatal("expected activation account id to be different") } if !account.DidSign(ac) { t.Fatal("expected account to have signed activation") } ac.IssuerAccount = publicKey(createUserNKey(t), t) ac.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected validation error") } } func TestCleanSubject(t *testing.T) { input := [][]string{ {"foo", "foo"}, {"*", "_"}, {">", "_"}, {"foo.*", "foo"}, {"foo.bar.>", "foo.bar"}, {"foo.*.bar", "foo"}, {"bam.boom.blat.*", "bam.boom.blat"}, {"*.blam", "_"}, } for _, pair := range input { clean := cleanSubject(pair[0]) if pair[1] != clean { t.Errorf("Expected %s but got %s", pair[1], clean) } } } func TestActivationClaimRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) e := &Export{Subject: "q.>", Type: Service, TokenReq: true} account.Exports.Add(e) a := publicKey(createAccountNKey(t), t) aminAgo := time.Now().Add(-time.Minute) if account.Exports[0].Revocations.IsRevoked(a, aminAgo) { t.Fatal("should not be revoked") } e.RevokeAt(a, aminAgo) if !account.Exports[0].Revocations.IsRevoked(a, aminAgo) { t.Fatal("should be revoked") } a2 := publicKey(createAccountNKey(t), t) if account.Exports[0].Revocations.IsRevoked(a2, aminAgo) { t.Fatal("should not be revoked") } e.RevokeAt("*", aminAgo) if !account.Exports[0].Revocations.IsRevoked(a2, time.Now().Add(-time.Hour)) { t.Fatal("should be revoked") } vr := ValidationResults{} account.Validate(&vr) if !vr.IsEmpty() { t.Fatal("account validation shouldn't have failed") } } func TestActivationClaimsSignFn(t *testing.T) { akp := createAccountNKey(t) target := createAccountNKey(t) act := NewActivationClaims(publicKey(target, t)) act.ImportSubject = "foo" act.ImportType = Stream ok := false s, err := act.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { ok = true if pub != publicKey(akp, t) { t.Fatal("expected pub key to match account") } return akp.Sign(data) }) if err != nil { t.Fatal(err) } if !ok { t.Fatal("expected ok to be true") } act, err = DecodeActivationClaims(s) if err != nil { t.Fatal(err) } vr := CreateValidationResults() act.Validate(vr) if !vr.IsEmpty() { t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) } } jwt-2.7.3/v2/authorization_claims.go000066400000000000000000000205211472706253100174440ustar00rootroot00000000000000/* * Copyright 2022-2024 The NATS Authors * 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. */ package jwt import ( "errors" "github.com/nats-io/nkeys" ) // ServerID is basic static info for a NATS server. type ServerID struct { Name string `json:"name"` Host string `json:"host"` ID string `json:"id"` Version string `json:"version,omitempty"` Cluster string `json:"cluster,omitempty"` Tags TagList `json:"tags,omitempty"` XKey string `json:"xkey,omitempty"` } // ClientInformation is information about a client that is trying to authorize. type ClientInformation struct { Host string `json:"host,omitempty"` ID uint64 `json:"id,omitempty"` User string `json:"user,omitempty"` Name string `json:"name,omitempty"` Tags TagList `json:"tags,omitempty"` NameTag string `json:"name_tag,omitempty"` Kind string `json:"kind,omitempty"` Type string `json:"type,omitempty"` MQTT string `json:"mqtt_id,omitempty"` Nonce string `json:"nonce,omitempty"` } // ConnectOptions represents options that were set in the CONNECT protocol from the client // during authorization. type ConnectOptions struct { JWT string `json:"jwt,omitempty"` Nkey string `json:"nkey,omitempty"` SignedNonce string `json:"sig,omitempty"` Token string `json:"auth_token,omitempty"` Username string `json:"user,omitempty"` Password string `json:"pass,omitempty"` Name string `json:"name,omitempty"` Lang string `json:"lang,omitempty"` Version string `json:"version,omitempty"` Protocol int `json:"protocol"` } // ClientTLS is information about TLS state if present, including client certs. // If the client certs were present and verified they will be under verified chains // with the client peer cert being VerifiedChains[0]. These are complete and pem encoded. // If they were not verified, they will be under certs. type ClientTLS struct { Version string `json:"version,omitempty"` Cipher string `json:"cipher,omitempty"` Certs StringList `json:"certs,omitempty"` VerifiedChains []StringList `json:"verified_chains,omitempty"` } // AuthorizationRequest represents all the information we know about the client that // will be sent to an external authorization service. type AuthorizationRequest struct { Server ServerID `json:"server_id"` UserNkey string `json:"user_nkey"` ClientInformation ClientInformation `json:"client_info"` ConnectOptions ConnectOptions `json:"connect_opts"` TLS *ClientTLS `json:"client_tls,omitempty"` RequestNonce string `json:"request_nonce,omitempty"` GenericFields } // AuthorizationRequestClaims defines an external auth request JWT. // These wil be signed by a NATS server. type AuthorizationRequestClaims struct { ClaimsData AuthorizationRequest `json:"nats"` } // NewAuthorizationRequestClaims creates an auth request JWT with the specific subject/public key. func NewAuthorizationRequestClaims(subject string) *AuthorizationRequestClaims { if subject == "" { return nil } var ac AuthorizationRequestClaims ac.Subject = subject return &ac } // Validate checks the generic and specific parts of the auth request jwt. func (ac *AuthorizationRequestClaims) Validate(vr *ValidationResults) { if ac.UserNkey == "" { vr.AddError("User nkey is required") } else if !nkeys.IsValidPublicUserKey(ac.UserNkey) { vr.AddError("User nkey %q is not a valid user public key", ac.UserNkey) } ac.ClaimsData.Validate(vr) } // Encode tries to turn the auth request claims into a JWT string. func (ac *AuthorizationRequestClaims) Encode(pair nkeys.KeyPair) (string, error) { return ac.EncodeWithSigner(pair, nil) } func (ac *AuthorizationRequestClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { ac.Type = AuthorizationRequestClaim return ac.ClaimsData.encode(pair, ac, fn) } // DecodeAuthorizationRequestClaims tries to parse an auth request claims from a JWT string func DecodeAuthorizationRequestClaims(token string) (*AuthorizationRequestClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } ac, ok := claims.(*AuthorizationRequestClaims) if !ok { return nil, errors.New("not an authorization request claim") } return ac, nil } // ExpectedPrefixes defines the types that can encode an auth request jwt, servers. func (ac *AuthorizationRequestClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteServer} } func (ac *AuthorizationRequestClaims) ClaimType() ClaimType { return ac.Type } // Claims returns the request claims data. func (ac *AuthorizationRequestClaims) Claims() *ClaimsData { return &ac.ClaimsData } // Payload pulls the request specific payload out of the claims. func (ac *AuthorizationRequestClaims) Payload() interface{} { return &ac.AuthorizationRequest } func (ac *AuthorizationRequestClaims) String() string { return ac.ClaimsData.String(ac) } func (ac *AuthorizationRequestClaims) updateVersion() { ac.GenericFields.Version = libVersion } type AuthorizationResponse struct { Jwt string `json:"jwt,omitempty"` Error string `json:"error,omitempty"` // IssuerAccount stores the public key for the account the issuer represents. // When set, the claim was issued by a signing key. IssuerAccount string `json:"issuer_account,omitempty"` GenericFields } type AuthorizationResponseClaims struct { ClaimsData AuthorizationResponse `json:"nats"` } func NewAuthorizationResponseClaims(subject string) *AuthorizationResponseClaims { if subject == "" { return nil } var ac AuthorizationResponseClaims ac.Subject = subject return &ac } // DecodeAuthorizationResponseClaims tries to parse an auth request claims from a JWT string func DecodeAuthorizationResponseClaims(token string) (*AuthorizationResponseClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } ac, ok := claims.(*AuthorizationResponseClaims) if !ok { return nil, errors.New("not an authorization request claim") } return ac, nil } // ExpectedPrefixes defines the types that can encode an auth request jwt, servers. func (ar *AuthorizationResponseClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount} } func (ar *AuthorizationResponseClaims) ClaimType() ClaimType { return ar.Type } // Claims returns the request claims data. func (ar *AuthorizationResponseClaims) Claims() *ClaimsData { return &ar.ClaimsData } // Payload pulls the request specific payload out of the claims. func (ar *AuthorizationResponseClaims) Payload() interface{} { return &ar.AuthorizationResponse } func (ar *AuthorizationResponseClaims) String() string { return ar.ClaimsData.String(ar) } func (ar *AuthorizationResponseClaims) updateVersion() { ar.GenericFields.Version = libVersion } // Validate checks the generic and specific parts of the auth request jwt. func (ar *AuthorizationResponseClaims) Validate(vr *ValidationResults) { if !nkeys.IsValidPublicUserKey(ar.Subject) { vr.AddError("Subject must be a user public key") } if !nkeys.IsValidPublicServerKey(ar.Audience) { vr.AddError("Audience must be a server public key") } if ar.Error == "" && ar.Jwt == "" { vr.AddError("Error or Jwt is required") } if ar.Error != "" && ar.Jwt != "" { vr.AddError("Only Error or Jwt can be set") } if ar.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(ar.IssuerAccount) { vr.AddError("issuer_account is not an account public key") } ar.ClaimsData.Validate(vr) } // Encode tries to turn the auth request claims into a JWT string. func (ar *AuthorizationResponseClaims) Encode(pair nkeys.KeyPair) (string, error) { return ar.EncodeWithSigner(pair, nil) } func (ar *AuthorizationResponseClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { ar.Type = AuthorizationResponseClaim return ar.ClaimsData.encode(pair, ar, fn) } jwt-2.7.3/v2/authorization_claims_test.go000066400000000000000000000130421472706253100205030ustar00rootroot00000000000000/* * Copyright 2022-2024 The NATS Authors * 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. */ package jwt import ( "testing" "github.com/nats-io/nkeys" ) func TestNewAuthorizationRequestClaims(t *testing.T) { skp, _ := nkeys.CreateServer() kp, err := nkeys.CreateUser() if err != nil { t.Fatalf("Error creating user: %v", err) } pub, _ := kp.PublicKey() // the subject of the claim is the user we are generating an authorization response ac := NewAuthorizationRequestClaims(pub) ac.Server.Name = "NATS-1" vr := CreateValidationResults() // Make sure that user nkey is required. ac.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatalf("Expected blocking error on an nkey user not being specified") } // Make sure it is required to be valid public user nkey. ac.UserNkey = "derek" vr = CreateValidationResults() ac.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatalf("Expected blocking error on invalid user nkey") } ac.UserNkey = pub vr = CreateValidationResults() ac.Validate(vr) if !vr.IsEmpty() { t.Fatal("Valid authorization request will have no validation results") } acJWT := encode(ac, skp, t) ac2, err := DecodeAuthorizationRequestClaims(acJWT) if err != nil { t.Fatal("error decoding authorization request jwt", err) } AssertEquals(ac.String(), ac2.String(), t) AssertEquals(ac.Server.Name, ac2.Server.Name, t) } func TestAuthorizationResponse_EmptyShouldFail(t *testing.T) { rc := NewAuthorizationResponseClaims("$G") vr := CreateValidationResults() rc.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatal("Expected blocking errors") } errs := vr.Errors() AssertEquals(3, len(errs), t) AssertEquals("Subject must be a user public key", errs[0].Error(), t) AssertEquals("Audience must be a server public key", errs[1].Error(), t) AssertEquals("Error or Jwt is required", errs[2].Error(), t) } func TestAuthorizationResponse_SubjMustBeServer(t *testing.T) { rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) rc.Error = "bad" vr := CreateValidationResults() rc.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatal("Expected blocking errors") } errs := vr.Errors() AssertEquals(1, len(errs), t) AssertEquals("Audience must be a server public key", errs[0].Error(), t) rc = NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) rc.Audience = publicKey(createServerNKey(t), t) rc.Error = "bad" vr = CreateValidationResults() rc.Validate(vr) AssertEquals(true, vr.IsEmpty(), t) } func TestAuthorizationResponse_OneOfErrOrJwt(t *testing.T) { rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) rc.Audience = publicKey(createServerNKey(t), t) rc.Error = "bad" rc.Jwt = "jwt" vr := CreateValidationResults() rc.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatal("Expected blocking errors") } errs := vr.Errors() AssertEquals(1, len(errs), t) AssertEquals("Only Error or Jwt can be set", errs[0].Error(), t) } func TestAuthorizationResponse_IssuerAccount(t *testing.T) { rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) rc.Audience = publicKey(createServerNKey(t), t) rc.Jwt = "jwt" rc.IssuerAccount = rc.Subject vr := CreateValidationResults() rc.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Fatal("Expected blocking errors") } errs := vr.Errors() AssertEquals(1, len(errs), t) AssertEquals("issuer_account is not an account public key", errs[0].Error(), t) akp := createAccountNKey(t) rc.IssuerAccount = publicKey(akp, t) vr = CreateValidationResults() rc.Validate(vr) AssertEquals(true, vr.IsEmpty(), t) } func TestAuthorizationResponse_Decode(t *testing.T) { rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) rc.Audience = publicKey(createServerNKey(t), t) rc.Jwt = "jwt" akp := createAccountNKey(t) tok, err := rc.Encode(akp) AssertNoError(err, t) r, err := DecodeAuthorizationResponseClaims(tok) AssertNoError(err, t) vr := CreateValidationResults() r.Validate(vr) AssertEquals(true, vr.IsEmpty(), t) AssertEquals("jwt", r.Jwt, t) AssertTrue(nkeys.IsValidPublicUserKey(r.Subject), t) AssertTrue(nkeys.IsValidPublicServerKey(r.Audience), t) } func TestNewAuthorizationRequestSignerFn(t *testing.T) { skp, _ := nkeys.CreateServer() kp, err := nkeys.CreateUser() if err != nil { t.Fatalf("Error creating user: %v", err) } // the subject of the claim is the user we are generating an authorization response ac := NewAuthorizationRequestClaims(publicKey(kp, t)) ac.Server.Name = "NATS-1" ac.UserNkey = publicKey(kp, t) ok := false ar, err := ac.EncodeWithSigner(skp, func(pub string, data []byte) ([]byte, error) { ok = true return skp.Sign(data) }) if err != nil { t.Fatal("error signing request") } if !ok { t.Fatal("not signed by signer function") } ac2, err := DecodeAuthorizationRequestClaims(ar) if err != nil { t.Fatal("error decoding authorization request jwt", err) } vr := CreateValidationResults() ac2.Validate(vr) if !vr.IsEmpty() { t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) } } jwt-2.7.3/v2/claims.go000066400000000000000000000167441472706253100145000ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "crypto/sha512" "encoding/base32" "encoding/base64" "encoding/json" "errors" "fmt" "time" "github.com/nats-io/nkeys" ) // ClaimType is used to indicate the type of JWT being stored in a Claim type ClaimType string const ( // OperatorClaim is the type of an operator JWT OperatorClaim = "operator" // AccountClaim is the type of an Account JWT AccountClaim = "account" // UserClaim is the type of an user JWT UserClaim = "user" // ActivationClaim is the type of an activation JWT ActivationClaim = "activation" // AuthorizationRequestClaim is the type of an auth request claim JWT AuthorizationRequestClaim = "authorization_request" // AuthorizationResponseClaim is the response for an auth request AuthorizationResponseClaim = "authorization_response" // GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim GenericClaim = "generic" ) func IsGenericClaimType(s string) bool { switch s { case OperatorClaim: fallthrough case AccountClaim: fallthrough case UserClaim: fallthrough case AuthorizationRequestClaim: fallthrough case AuthorizationResponseClaim: fallthrough case ActivationClaim: return false case GenericClaim: return true default: return true } } // SignFn is used in an external sign environment. The function should be // able to locate the private key for the specified pub key specified and sign the // specified data returning the signature as generated. type SignFn func(pub string, data []byte) ([]byte, error) // Claims is a JWT claims type Claims interface { Claims() *ClaimsData Encode(kp nkeys.KeyPair) (string, error) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) ExpectedPrefixes() []nkeys.PrefixByte Payload() interface{} String() string Validate(vr *ValidationResults) ClaimType() ClaimType verify(payload string, sig []byte) bool updateVersion() } type GenericFields struct { Tags TagList `json:"tags,omitempty"` Type ClaimType `json:"type,omitempty"` Version int `json:"version,omitempty"` } // ClaimsData is the base struct for all claims type ClaimsData struct { Audience string `json:"aud,omitempty"` Expires int64 `json:"exp,omitempty"` ID string `json:"jti,omitempty"` IssuedAt int64 `json:"iat,omitempty"` Issuer string `json:"iss,omitempty"` Name string `json:"name,omitempty"` NotBefore int64 `json:"nbf,omitempty"` Subject string `json:"sub,omitempty"` } // Prefix holds the prefix byte for an NKey type Prefix struct { nkeys.PrefixByte } func encodeToString(d []byte) string { return base64.RawURLEncoding.EncodeToString(d) } func decodeString(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(s) } func serialize(v interface{}) (string, error) { j, err := json.Marshal(v) if err != nil { return "", err } return encodeToString(j), nil } func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims, fn SignFn) (string, error) { if header == nil { return "", errors.New("header is required") } if kp == nil { return "", errors.New("keypair is required") } if c != claim.Claims() { return "", errors.New("claim and claim data do not match") } if c.Subject == "" { return "", errors.New("subject is not set") } h, err := serialize(header) if err != nil { return "", err } issuerBytes, err := kp.PublicKey() if err != nil { return "", err } prefixes := claim.ExpectedPrefixes() if prefixes != nil { ok := false for _, p := range prefixes { switch p { case nkeys.PrefixByteAccount: if nkeys.IsValidPublicAccountKey(issuerBytes) { ok = true } case nkeys.PrefixByteOperator: if nkeys.IsValidPublicOperatorKey(issuerBytes) { ok = true } case nkeys.PrefixByteServer: if nkeys.IsValidPublicServerKey(issuerBytes) { ok = true } case nkeys.PrefixByteCluster: if nkeys.IsValidPublicClusterKey(issuerBytes) { ok = true } case nkeys.PrefixByteUser: if nkeys.IsValidPublicUserKey(issuerBytes) { ok = true } } } if !ok { return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes) } } c.Issuer = issuerBytes c.IssuedAt = time.Now().UTC().Unix() c.ID = "" // to create a repeatable hash c.ID, err = c.hash() if err != nil { return "", err } claim.updateVersion() payload, err := serialize(claim) if err != nil { return "", err } toSign := fmt.Sprintf("%s.%s", h, payload) eSig := "" if header.Algorithm == AlgorithmNkeyOld { return "", errors.New(AlgorithmNkeyOld + " not supported to write jwtV2") } else if header.Algorithm == AlgorithmNkey { var sig []byte if fn != nil { pk, err := kp.PublicKey() if err != nil { return "", err } sig, err = fn(pk, []byte(toSign)) if err != nil { return "", err } } else { sig, err = kp.Sign([]byte(toSign)) if err != nil { return "", err } } eSig = encodeToString(sig) } else { return "", errors.New(header.Algorithm + " not supported to write jwtV2") } // hash need no padding return fmt.Sprintf("%s.%s", toSign, eSig), nil } func (c *ClaimsData) hash() (string, error) { j, err := json.Marshal(c) if err != nil { return "", err } h := sha512.New512_256() h.Write(j) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil } // Encode encodes a claim into a JWT token. The claim is signed with the // provided nkey's private key func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims, fn SignFn) (string, error) { return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload, fn) } // Returns a JSON representation of the claim func (c *ClaimsData) String(claim interface{}) string { j, err := json.MarshalIndent(claim, "", " ") if err != nil { return "" } return string(j) } func parseClaims(s string, target Claims) error { h, err := decodeString(s) if err != nil { return err } return json.Unmarshal(h, &target) } // Verify verifies that the encoded payload was signed by the // provided public key. Verify is called automatically with // the claims portion of the token and the public key in the claim. // Client code need to insure that the public key in the // claim is trusted. func (c *ClaimsData) verify(payload string, sig []byte) bool { // decode the public key kp, err := nkeys.FromPublicKey(c.Issuer) if err != nil { return false } if err := kp.Verify([]byte(payload), sig); err != nil { return false } return true } // Validate checks a claim to make sure it is valid. Validity checks // include expiration and not before constraints. func (c *ClaimsData) Validate(vr *ValidationResults) { now := time.Now().UTC().Unix() if c.Expires > 0 && now > c.Expires { vr.AddTimeCheck("claim is expired") } if c.NotBefore > 0 && c.NotBefore > now { vr.AddTimeCheck("claim is not yet valid") } } // IsSelfSigned returns true if the claims issuer is the subject func (c *ClaimsData) IsSelfSigned() bool { return c.Issuer == c.Subject } jwt-2.7.3/v2/creds_utils.go000066400000000000000000000174021472706253100155400ustar00rootroot00000000000000/* * Copyright 2019-2020 The NATS Authors * 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. */ package jwt import ( "bytes" "errors" "fmt" "regexp" "strings" "time" "github.com/nats-io/nkeys" ) // DecorateJWT returns a decorated JWT that describes the kind of JWT func DecorateJWT(jwtString string) ([]byte, error) { gc, err := Decode(jwtString) if err != nil { return nil, err } return formatJwt(string(gc.ClaimType()), jwtString) } func formatJwt(kind string, jwtString string) ([]byte, error) { templ := `-----BEGIN NATS %s JWT----- %s ------END NATS %s JWT------ ` w := bytes.NewBuffer(nil) kind = strings.ToUpper(kind) _, err := fmt.Fprintf(w, templ, kind, jwtString, kind) if err != nil { return nil, err } return w.Bytes(), nil } // DecorateSeed takes a seed and returns a string that wraps // the seed in the form: // // ************************* IMPORTANT ************************* // NKEY Seed printed below can be used sign and prove identity. // NKEYs are sensitive and should be treated as secrets. // // -----BEGIN USER NKEY SEED----- // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM // ------END USER NKEY SEED------ func DecorateSeed(seed []byte) ([]byte, error) { w := bytes.NewBuffer(nil) ts := bytes.TrimSpace(seed) pre := string(ts[0:2]) kind := "" switch pre { case "SU": kind = "USER" case "SA": kind = "ACCOUNT" case "SO": kind = "OPERATOR" default: return nil, errors.New("seed is not an operator, account or user seed") } header := `************************* IMPORTANT ************************* NKEY Seed printed below can be used to sign and prove identity. NKEYs are sensitive and should be treated as secrets. -----BEGIN %s NKEY SEED----- ` _, err := fmt.Fprintf(w, header, kind) if err != nil { return nil, err } w.Write(ts) footer := ` ------END %s NKEY SEED------ ************************************************************* ` _, err = fmt.Fprintf(w, footer, kind) if err != nil { return nil, err } return w.Bytes(), nil } var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`) // An user config file looks like this: // -----BEGIN NATS USER JWT----- // eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5... // ------END NATS USER JWT------ // // ************************* IMPORTANT ************************* // NKEY Seed printed below can be used sign and prove identity. // NKEYs are sensitive and should be treated as secrets. // // -----BEGIN USER NKEY SEED----- // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM // ------END USER NKEY SEED------ // FormatUserConfig returns a decorated file with a decorated JWT and decorated seed func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) { gc, err := Decode(jwtString) if err != nil { return nil, err } if gc.ClaimType() != UserClaim { return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.ClaimType())) } w := bytes.NewBuffer(nil) jd, err := formatJwt(string(gc.ClaimType()), jwtString) if err != nil { return nil, err } _, err = w.Write(jd) if err != nil { return nil, err } if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) { return nil, fmt.Errorf("nkey seed is not an user seed") } d, err := DecorateSeed(seed) if err != nil { return nil, err } _, err = w.Write(d) if err != nil { return nil, err } return w.Bytes(), nil } // ParseDecoratedJWT takes a creds file and returns the JWT portion. func ParseDecoratedJWT(contents []byte) (string, error) { items := userConfigRE.FindAllSubmatch(contents, -1) if len(items) == 0 { return string(contents), nil } // First result should be the user JWT. // We copy here so that if the file contained a seed file too we wipe appropriately. raw := items[0][1] tmp := make([]byte, len(raw)) copy(tmp, raw) return string(tmp), nil } // ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a // key pair from it. func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) { var seed []byte items := userConfigRE.FindAllSubmatch(contents, -1) if len(items) > 1 { seed = items[1][1] } else { lines := bytes.Split(contents, []byte("\n")) for _, line := range lines { if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) || bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) || bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) { seed = line break } } } if seed == nil { return nil, errors.New("no nkey seed found") } if !bytes.HasPrefix(seed, []byte("SO")) && !bytes.HasPrefix(seed, []byte("SA")) && !bytes.HasPrefix(seed, []byte("SU")) { return nil, errors.New("doesn't contain a seed nkey") } kp, err := nkeys.FromSeed(seed) if err != nil { return nil, err } return kp, nil } // ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a // key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys. func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) { nk, err := ParseDecoratedNKey(contents) if err != nil { return nil, err } seed, err := nk.Seed() if err != nil { return nil, err } if !bytes.HasPrefix(seed, []byte("SU")) { return nil, errors.New("doesn't contain an user seed nkey") } kp, err := nkeys.FromSeed(seed) if err != nil { return nil, err } return kp, nil } // IssueUserJWT takes an account scoped signing key, account id, and use public key (and optionally a user's name, an expiration duration and tags) and returns a valid signed JWT. // The scopedSigningKey, is a mandatory account scoped signing nkey pair to sign the generated jwt (note that it _must_ be a signing key attached to the account (and a _scoped_ signing key), not the account's private (seed) key). // The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey. // The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey. // The name, is an optional human-readable name. When absent, default to publicUserKey. // The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. // The tags, is an optional list of tags to be included in the JWT. // // Returns: // string, resulting jwt. // error, when issues arose. func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) { if !nkeys.IsValidPublicAccountKey(accountId) { return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String()) } if !nkeys.IsValidPublicUserKey(publicUserKey) { return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String()) } claim := NewUserClaims(publicUserKey) claim.SetScoped(true) if expirationDuration != 0 { claim.Expires = time.Now().Add(expirationDuration).UTC().Unix() } claim.IssuerAccount = accountId if name != "" { claim.Name = name } else { claim.Name = publicUserKey } claim.Subject = publicUserKey claim.Tags = tags encoded, err := claim.Encode(scopedSigningKey) if err != nil { return "", errors.New("err encoding claim " + err.Error()) } return encoded, nil } jwt-2.7.3/v2/creds_utils_test.go000066400000000000000000000204761472706253100166040ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "bytes" "fmt" "strings" "testing" "github.com/nats-io/nkeys" ) func makeJWT(t *testing.T) (string, nkeys.KeyPair) { akp := createAccountNKey(t) kp := createUserNKey(t) pk := publicKey(kp, t) oc := NewUserClaims(pk) token, err := oc.Encode(akp) if err != nil { t.Fatal(err) } return token, kp } func Test_DecorateJwt(t *testing.T) { token, _ := makeJWT(t) d, err := DecorateJWT(token) if err != nil { t.Fatal(err) } s := string(d) if !strings.Contains(s, "-BEGIN NATS USER JWT-") { t.Fatal("doesn't contain expected header") } if !strings.Contains(s, "eyJ0") { t.Fatal("doesn't contain public key") } if !strings.Contains(s, "-END NATS USER JWT------\n\n") { t.Fatal("doesn't contain expected footer") } } func Test_FormatUserConfig(t *testing.T) { token, kp := makeJWT(t) d, err := FormatUserConfig(token, seedKey(kp, t)) if err != nil { t.Fatal(err) } s := string(d) if !strings.Contains(s, "-BEGIN NATS USER JWT-") { t.Fatal("doesn't contain expected header") } if !strings.Contains(s, "eyJ0") { t.Fatal("doesn't contain public key") } if !strings.Contains(s, "-END NATS USER JWT-") { t.Fatal("doesn't contain expected footer") } validateSeed(t, d, kp) } func validateSeed(t *testing.T, decorated []byte, nk nkeys.KeyPair) { kind := "" seed := seedKey(nk, t) switch string(seed[0:2]) { case "SO": kind = "operator" case "SA": kind = "account" case "SU": kind = "user" default: kind = "not supported" } kind = strings.ToUpper(kind) s := string(decorated) if !strings.Contains(s, fmt.Sprintf("\n\n-----BEGIN %s NKEY SEED-", kind)) { t.Fatal("doesn't contain expected seed header") } if !strings.Contains(s, string(seed)) { t.Fatal("doesn't contain the seed") } if !strings.Contains(s, fmt.Sprintf("-END %s NKEY SEED------\n\n", kind)) { t.Fatal("doesn't contain expected seed footer") } } func Test_ParseDecoratedJWT(t *testing.T) { token, _ := makeJWT(t) t2, err := ParseDecoratedJWT([]byte(token)) if err != nil { t.Fatal(err) } if token != t2 { t.Fatal("jwt didn't match expected") } decorated, err := DecorateJWT(token) if err != nil { t.Fatal(err) } t3, err := ParseDecoratedJWT(decorated) if err != nil { t.Fatal(err) } if token != t3 { t.Fatal("parse decorated jwt didn't match expected") } } func Test_ParseDecoratedJWTBad(t *testing.T) { v, err := ParseDecoratedJWT([]byte("foo")) if err != nil { t.Fatal(err) } if v != "foo" { t.Fatal("unexpected input was not returned") } } func Test_ParseDecoratedOPJWT(t *testing.T) { content := []string{ `-----BEGIN TEST OPERATOR JWT----- eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJhdWQiOiJURVNUUyIsImV4cCI6MTg1OTEyMTI3NSwianRpIjoiWE5MWjZYWVBIVE1ESlFSTlFPSFVPSlFHV0NVN01JNVc1SlhDWk5YQllVS0VRVzY3STI1USIsImlhdCI6MTU0Mzc2MTI3NSwiaXNzIjoiT0NBVDMzTVRWVTJWVU9JTUdOR1VOWEo2NkFIMlJMU0RBRjNNVUJDWUFZNVFNSUw2NU5RTTZYUUciLCJuYW1lIjoiU3luYWRpYSBDb21tdW5pY2F0aW9ucyBJbmMuIiwibmJmIjoxNTQzNzYxMjc1LCJzdWIiOiJPQ0FUMzNNVFZVMlZVT0lNR05HVU5YSjY2QUgyUkxTREFGM01VQkNZQVk1UU1JTDY1TlFNNlhRRyIsInR5cGUiOiJvcGVyYXRvciIsIm5hdHMiOnsic2lnbmluZ19rZXlzIjpbIk9EU0tSN01ZRlFaNU1NQUo2RlBNRUVUQ1RFM1JJSE9GTFRZUEpSTUFWVk40T0xWMllZQU1IQ0FDIiwiT0RTS0FDU1JCV1A1MzdEWkRSVko2NTdKT0lHT1BPUTZLRzdUNEhONk9LNEY2SUVDR1hEQUhOUDIiLCJPRFNLSTM2TFpCNDRPWTVJVkNSNlA1MkZaSlpZTVlXWlZXTlVEVExFWjVUSzJQTjNPRU1SVEFCUiJdfX0.hyfz6E39BMUh0GLzovFfk3wT4OfualftjdJ_eYkLfPvu5tZubYQ_Pn9oFYGCV_6yKy3KMGhWGUCyCdHaPhalBw ------END TEST OPERATOR JWT------`, `-----BEGIN TEST OPERATOR JWT----- eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJKV01TUzNRUFpDS0lHSE1BWko3RUpQSlVHN01DTFNQUkJaTEpSUUlRQkRVTkFaUE5MQVVBIiwiaWF0IjoxNTY1ODg5NzEyLCJpc3MiOiJPQU01VlNINDJXRlZWTkpXNFNMRTZRVkpCREpVRTJGUVNYWkxRTk1SRDdBMlBaTTIzTDIyWFlVWSIsIm5hbWUiOiJzeW5hZGlhIiwic3ViIjoiT0FNNVZTSDQyV0ZWVk5KVzRTTEU2UVZKQkRKVUUyRlFTWFpMUU5NUkQ3QTJQWk0yM0wyMlhZVVkiLCJ0eXBlIjoib3BlcmF0b3IiLCJuYXRzIjp7ImFjY291bnRfc2VydmVyX3VybCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjA2MC9qd3QvdjEiLCJvcGVyYXRvcl9zZXJ2aWNlX3VybHMiOlsibmF0czovL2xvY2FsaG9zdDo0MTQxIl19fQ.XPvAezQj3AxwEvYLVBq-EIssP4OhjoMGLbIaripzBKv1oCtHdPNKz96YwB2vUoY-4OrN9ZOPo9TKR3jVxq0uBQ ------END TEST OPERATOR JWT------`} test := func(content string) { t.Helper() v, err := ParseDecoratedJWT([]byte(content)) if err != nil { t.Fatal(err) } if !strings.HasPrefix(v, "eyJ") { t.Fatal("unexpected input was not returned") } } for i, cont := range content { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { test(cont) }) t.Run(fmt.Sprintf("%d-win", i), func(t *testing.T) { test(strings.ReplaceAll(cont, "\n", "\r\n")) }) cont = cont + "\n" t.Run(fmt.Sprintf("%d-trail-nl", i), func(t *testing.T) { test(cont) }) t.Run(fmt.Sprintf("%d-trail-nl-win", i), func(t *testing.T) { test(strings.ReplaceAll(cont, "\n", "\r\n")) }) } } func Test_ParseDecoratedSeed(t *testing.T) { token, ukp := makeJWT(t) us := seedKey(ukp, t) decorated, err := FormatUserConfig(token, us) if err != nil { t.Fatal(err) } kp, err := ParseDecoratedUserNKey(decorated) if err != nil { t.Fatal(err) } pu := seedKey(kp, t) if !bytes.Equal(us, pu) { t.Fatal("seeds don't match") } } func Test_ParseDecoratedBadKey(t *testing.T) { token, ukp := makeJWT(t) us, err := ukp.Seed() if err != nil { t.Fatal(err) } akp := createAccountNKey(t) as := seedKey(akp, t) _, err = FormatUserConfig(token, as) if err == nil { t.Fatal("should have failed to encode with bad seed") } sc, err := FormatUserConfig(token, us) if err != nil { t.Fatal(err) } bad := strings.Replace(string(sc), string(us), string(as), -1) _, err = ParseDecoratedUserNKey([]byte(bad)) if err == nil { t.Fatal("parse should have failed for non user nkey") } } func Test_FailsOnNonUserJWT(t *testing.T) { akp := createAccountNKey(t) pk := publicKey(akp, t) ac := NewAccountClaims(pk) token, err := ac.Encode(akp) if err != nil { t.Fatal(err) } ukp := createUserNKey(t) us := seedKey(ukp, t) _, err = FormatUserConfig(token, us) if err == nil { t.Fatal("should have failed with account claims") } } func Test_DecorateNKeys(t *testing.T) { var kps []nkeys.KeyPair kps = append(kps, createOperatorNKey(t)) kps = append(kps, createAccountNKey(t)) kps = append(kps, createUserNKey(t)) for _, kp := range kps { seed := seedKey(kp, t) d, err := DecorateSeed(seed) if err != nil { t.Fatal(err, string(seed)) } validateSeed(t, d, kp) kp2, err := ParseDecoratedNKey(d) if err != nil { t.Fatal(string(seed), err) } seed2 := seedKey(kp2, t) if !bytes.Equal(seed, seed2) { t.Fatalf("seeds dont match %q != %q", string(seed), string(seed2)) } } _, err := ParseDecoratedNKey([]byte("bad")) if err == nil { t.Fatal("required error parsing bad nkey") } } func Test_ParseCreds(t *testing.T) { token, kp := makeJWT(t) d, err := FormatUserConfig(token, seedKey(kp, t)) if err != nil { t.Fatal(err) } pk, err := kp.PublicKey() if err != nil { t.Fatal(err) } token2, err := ParseDecoratedJWT(d) if err != nil { t.Fatal(err) } if token != token2 { t.Fatal("expected jwts to match") } kp2, err := ParseDecoratedUserNKey(d) if err != nil { t.Fatal(err) } pk2, err := kp2.PublicKey() if err != nil { t.Fatal(err) } if pk != pk2 { t.Fatal("expected keys to match") } } func Test_ParseCredsWithCrLfs(t *testing.T) { token, kp := makeJWT(t) d, err := FormatUserConfig(token, seedKey(kp, t)) if err != nil { t.Fatal(err) } pk, err := kp.PublicKey() if err != nil { t.Fatal(err) } d = bytes.ReplaceAll(d, []byte{'\n'}, []byte{'\r', '\n'}) token2, err := ParseDecoratedJWT(d) if err != nil { t.Fatal(err) } if token != token2 { t.Fatal("expected jwts to match") } kp2, err := ParseDecoratedUserNKey(d) if err != nil { t.Fatal(err) } pk2, err := kp2.PublicKey() if err != nil { t.Fatal(err) } if pk != pk2 { t.Fatal("expected keys to match") } } jwt-2.7.3/v2/decoder.go000066400000000000000000000077101472706253100146260ustar00rootroot00000000000000/* * Copyright 2020-2022 The NATS Authors * 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. */ package jwt import ( "encoding/json" "errors" "fmt" "strings" "github.com/nats-io/nkeys" ) const libVersion = 2 type identifier struct { Type ClaimType `json:"type,omitempty"` GenericFields `json:"nats,omitempty"` } func (i *identifier) Kind() ClaimType { if i.Type != "" { return i.Type } return i.GenericFields.Type } func (i *identifier) Version() int { if i.Type != "" { return 1 } return i.GenericFields.Version } type v1ClaimsDataDeletedFields struct { Tags TagList `json:"tags,omitempty"` Type ClaimType `json:"type,omitempty"` IssuerAccount string `json:"issuer_account,omitempty"` } // Decode takes a JWT string decodes it and validates it // and return the embedded Claims. If the token header // doesn't match the expected algorithm, or the claim is // not valid or verification fails an error is returned. func Decode(token string) (Claims, error) { // must have 3 chunks chunks := strings.Split(token, ".") if len(chunks) != 3 { return nil, errors.New("expected 3 chunks") } // header if _, err := parseHeaders(chunks[0]); err != nil { return nil, err } // claim data, err := decodeString(chunks[1]) if err != nil { return nil, err } ver, claim, err := loadClaims(data) if err != nil { return nil, err } // sig sig, err := decodeString(chunks[2]) if err != nil { return nil, err } if ver <= 1 { if !claim.verify(chunks[1], sig) { return nil, errors.New("claim failed V1 signature verification") } } else { if !claim.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) { return nil, errors.New("claim failed V2 signature verification") } } prefixes := claim.ExpectedPrefixes() if prefixes != nil { ok := false issuer := claim.Claims().Issuer for _, p := range prefixes { switch p { case nkeys.PrefixByteAccount: if nkeys.IsValidPublicAccountKey(issuer) { ok = true } case nkeys.PrefixByteOperator: if nkeys.IsValidPublicOperatorKey(issuer) { ok = true } case nkeys.PrefixByteUser: if nkeys.IsValidPublicUserKey(issuer) { ok = true } case nkeys.PrefixByteServer: if nkeys.IsValidPublicServerKey(issuer) { ok = true } } } if !ok { return nil, fmt.Errorf("unable to validate expected prefixes - %v", prefixes) } } return claim, nil } func loadClaims(data []byte) (int, Claims, error) { var id identifier if err := json.Unmarshal(data, &id); err != nil { return -1, nil, err } if id.Version() > libVersion { return -1, nil, errors.New("JWT was generated by a newer version ") } var claim Claims var err error switch id.Kind() { case OperatorClaim: claim, err = loadOperator(data, id.Version()) case AccountClaim: claim, err = loadAccount(data, id.Version()) case UserClaim: claim, err = loadUser(data, id.Version()) case ActivationClaim: claim, err = loadActivation(data, id.Version()) case AuthorizationRequestClaim: claim, err = loadAuthorizationRequest(data, id.Version()) case AuthorizationResponseClaim: claim, err = loadAuthorizationResponse(data, id.Version()) case "cluster": return -1, nil, errors.New("ClusterClaims are not supported") case "server": return -1, nil, errors.New("ServerClaims are not supported") default: var gc GenericClaims if err := json.Unmarshal(data, &gc); err != nil { return -1, nil, err } return -1, &gc, nil } return id.Version(), claim, err } jwt-2.7.3/v2/decoder_account.go000066400000000000000000000047541472706253100163470ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" ) type v1NatsAccount struct { Imports Imports `json:"imports,omitempty"` Exports Exports `json:"exports,omitempty"` Limits struct { NatsLimits AccountLimits } `json:"limits,omitempty"` SigningKeys StringList `json:"signing_keys,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` } func loadAccount(data []byte, version int) (*AccountClaims, error) { switch version { case 1: var v1a v1AccountClaims if err := json.Unmarshal(data, &v1a); err != nil { return nil, err } return v1a.Migrate() case 2: var v2a AccountClaims v2a.SigningKeys = make(SigningKeys) if err := json.Unmarshal(data, &v2a); err != nil { return nil, err } if len(v2a.Limits.JetStreamTieredLimits) > 0 { v2a.Limits.JetStreamLimits = JetStreamLimits{} } return &v2a, nil default: return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) } } type v1AccountClaims struct { ClaimsData v1ClaimsDataDeletedFields v1NatsAccount `json:"nats,omitempty"` } func (oa v1AccountClaims) Migrate() (*AccountClaims, error) { return oa.migrateV1() } func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) { var a AccountClaims // copy the base claim a.ClaimsData = oa.ClaimsData // move the moved fields a.Account.Type = oa.v1ClaimsDataDeletedFields.Type a.Account.Tags = oa.v1ClaimsDataDeletedFields.Tags // copy the account data a.Account.Imports = oa.v1NatsAccount.Imports a.Account.Exports = oa.v1NatsAccount.Exports a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits a.Account.Limits.JetStreamLimits = JetStreamLimits{} a.Account.SigningKeys = make(SigningKeys) for _, v := range oa.SigningKeys { a.Account.SigningKeys.Add(v) } a.Account.Revocations = oa.v1NatsAccount.Revocations a.Version = 1 return &a, nil } jwt-2.7.3/v2/decoder_activation.go000066400000000000000000000043041472706253100170430ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" ) // Migration adds GenericFields type v1NatsActivation struct { ImportSubject Subject `json:"subject,omitempty"` ImportType ExportType `json:"type,omitempty"` // Limit values deprecated inv v2 Max int64 `json:"max,omitempty"` Payload int64 `json:"payload,omitempty"` Src string `json:"src,omitempty"` Times []TimeRange `json:"times,omitempty"` } type v1ActivationClaims struct { ClaimsData v1ClaimsDataDeletedFields v1NatsActivation `json:"nats,omitempty"` } func loadActivation(data []byte, version int) (*ActivationClaims, error) { switch version { case 1: var v1a v1ActivationClaims v1a.Max = NoLimit v1a.Payload = NoLimit if err := json.Unmarshal(data, &v1a); err != nil { return nil, err } return v1a.Migrate() case 2: var v2a ActivationClaims if err := json.Unmarshal(data, &v2a); err != nil { return nil, err } return &v2a, nil default: return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) } } func (oa v1ActivationClaims) Migrate() (*ActivationClaims, error) { return oa.migrateV1() } func (oa v1ActivationClaims) migrateV1() (*ActivationClaims, error) { var a ActivationClaims // copy the base claim a.ClaimsData = oa.ClaimsData // move the moved fields a.Activation.Type = oa.v1ClaimsDataDeletedFields.Type a.Activation.Tags = oa.v1ClaimsDataDeletedFields.Tags a.Activation.IssuerAccount = oa.v1ClaimsDataDeletedFields.IssuerAccount // copy the activation data a.ImportSubject = oa.ImportSubject a.ImportType = oa.ImportType a.Version = 1 return &a, nil } jwt-2.7.3/v2/decoder_authorization.go000066400000000000000000000020721472706253100176020ustar00rootroot00000000000000/* * Copyright 2022 The NATS Authors * 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. */ package jwt import ( "encoding/json" ) func loadAuthorizationRequest(data []byte, version int) (*AuthorizationRequestClaims, error) { var ac AuthorizationRequestClaims if err := json.Unmarshal(data, &ac); err != nil { return nil, err } return &ac, nil } func loadAuthorizationResponse(data []byte, version int) (*AuthorizationResponseClaims, error) { var ac AuthorizationResponseClaims if err := json.Unmarshal(data, &ac); err != nil { return nil, err } return &ac, nil } jwt-2.7.3/v2/decoder_operator.go000066400000000000000000000042361472706253100165410ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" ) type v1NatsOperator struct { SigningKeys StringList `json:"signing_keys,omitempty"` AccountServerURL string `json:"account_server_url,omitempty"` OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` SystemAccount string `json:"system_account,omitempty"` } func loadOperator(data []byte, version int) (*OperatorClaims, error) { switch version { case 1: var v1a v1OperatorClaims if err := json.Unmarshal(data, &v1a); err != nil { return nil, err } return v1a.Migrate() case 2: var v2a OperatorClaims if err := json.Unmarshal(data, &v2a); err != nil { return nil, err } return &v2a, nil default: return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) } } type v1OperatorClaims struct { ClaimsData v1ClaimsDataDeletedFields v1NatsOperator `json:"nats,omitempty"` } func (oa v1OperatorClaims) Migrate() (*OperatorClaims, error) { return oa.migrateV1() } func (oa v1OperatorClaims) migrateV1() (*OperatorClaims, error) { var a OperatorClaims // copy the base claim a.ClaimsData = oa.ClaimsData // move the moved fields a.Operator.Type = oa.v1ClaimsDataDeletedFields.Type a.Operator.Tags = oa.v1ClaimsDataDeletedFields.Tags // copy the account data a.Operator.SigningKeys = oa.v1NatsOperator.SigningKeys a.Operator.AccountServerURL = oa.v1NatsOperator.AccountServerURL a.Operator.OperatorServiceURLs = oa.v1NatsOperator.OperatorServiceURLs a.Operator.SystemAccount = oa.v1NatsOperator.SystemAccount a.Version = 1 return &a, nil } jwt-2.7.3/v2/decoder_test.go000066400000000000000000000234601472706253100156650ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "reflect" "strings" "testing" "time" "github.com/nats-io/nkeys" ) func TestNewToken(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } claims := NewGenericClaims(publicKey(createUserNKey(t), t)) claims.Data["foo"] = "bar" token, err := claims.Encode(kp) if err != nil { t.Fatal("error encoding token", err) } c, err := DecodeGeneric(token) if err != nil { t.Fatal(err) } if claims.NotBefore != c.NotBefore { t.Fatal("notbefore don't match") } if claims.Issuer != c.Issuer { t.Fatal("notbefore don't match") } if !reflect.DeepEqual(claims.Data, c.Data) { t.Fatal("data sections don't match") } } func TestBadType(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{"JWS", AlgorithmNkey} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } claim, err := DecodeGeneric(token) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != fmt.Sprintf("not supported type %q", "JWS") { t.Fatal("expected not supported type error") } } func TestBadAlgo(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{TokenTypeJwt, "foobar"} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" if _, err := c.doEncode(&h, kp, c, nil); err == nil { t.Fatal("expected an error due to bad algorithm") } h = Header{TokenTypeJwt, AlgorithmNkeyOld} c = NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" if _, err := c.doEncode(&h, kp, c, nil); err == nil { t.Fatal("expected an error due to bad algorithm") } } func TestBadJWT(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{"JWS", AlgorithmNkey} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } chunks := strings.Split(token, ".") badToken := fmt.Sprintf("%s.%s", chunks[0], chunks[1]) claim, err := DecodeGeneric(badToken) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != "expected 3 chunks" { t.Fatalf("unexpeced error: %q", err.Error()) } } func TestBadSignature(t *testing.T) { kp := createAccountNKey(t) for algo, anErr := range map[string]string{ AlgorithmNkey: "claim failed V2 signature verification", } { h := Header{TokenTypeJwt, algo} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } token = token + "A" claim, err := DecodeGeneric(token) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != anErr { m := fmt.Sprintf("expected failed signature: %q", err.Error()) t.Fatal(m) } } } func TestDifferentPayload(t *testing.T) { akp1 := createAccountNKey(t) c1 := NewGenericClaims(publicKey(createUserNKey(t), t)) c1.Data["foo"] = "barz" jwt1 := encode(c1, akp1, t) c1t := strings.Split(jwt1, ".") c1.Data["foo"] = "bar" kp2 := createAccountNKey(t) token2 := encode(c1, kp2, t) c2t := strings.Split(token2, ".") c1t[1] = c2t[1] claim, err := DecodeGeneric(fmt.Sprintf("%s.%s.%s", c1t[0], c1t[1], c1t[2])) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != "claim failed V2 signature verification" { m := fmt.Sprintf("expected failed signature: %q", err.Error()) t.Fatal(m) } } func TestExpiredToken(t *testing.T) { akp := createAccountNKey(t) c := NewGenericClaims(publicKey(akp, t)) c.Expires = time.Now().UTC().Unix() - 100 c.Data["foo"] = "barz" vr := CreateValidationResults() c.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("expired tokens should be blocking when time is included") } if vr.IsBlocking(false) { t.Fatalf("expired tokens should not be blocking when time is not included") } } func TestNotYetValid(t *testing.T) { akp1, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } c := NewGenericClaims(publicKey(akp1, t)) c.NotBefore = time.Now().Add(time.Duration(1) * time.Hour).UTC().Unix() vr := CreateValidationResults() c.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("not yet valid tokens should be blocking when time is included") } if vr.IsBlocking(false) { t.Fatalf("not yet valid tokens should not be blocking when time is not included") } } func TestIssuedAtIsSet(t *testing.T) { akp := createAccountNKey(t) c := NewGenericClaims(publicKey(akp, t)) c.Data["foo"] = "barz" token, err := c.Encode(akp) if err != nil { t.Fatal(err) } claim, err := DecodeGeneric(token) if err != nil { t.Fatalf("unexpected error: %v", err) } if claim.IssuedAt == 0 { t.Fatalf("issued at is not set") } } func TestSample(t *testing.T) { // Need a private key to sign the claim akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) // add a bunch of claims claims.Data["foo"] = "bar" // serialize the claim to a JWT token token, err := claims.Encode(akp) if err != nil { t.Fatal("error encoding token", err) } // on the receiving side, decode the token c, err := DecodeGeneric(token) if err != nil { t.Fatal(err) } // if the token was decoded, it means that it // validated and it wasn't tampered. the remaining and // required test is to insure the issuer is trusted pk, err := akp.PublicKey() if err != nil { t.Fatalf("unable to read public key: %v", err) } if c.Issuer != pk { t.Fatalf("the public key is not trusted") } } func TestBadHeaderEncoding(t *testing.T) { // the '=' will be illegal _, err := parseHeaders("=hello=") if err == nil { t.Fatal("should have failed it is not encoded") } } func TestBadClaimsEncoding(t *testing.T) { // the '=' will be illegal c := GenericClaims{} err := parseClaims("=hello=", &c) if err == nil { t.Fatal("should have failed it is not encoded") } } func TestBadHeaderJSON(t *testing.T) { payload := encodeToString([]byte("{foo: bar}")) _, err := parseHeaders(payload) if err == nil { t.Fatal("should have failed bad json") } } func TestBadClaimsJSON(t *testing.T) { payload := encodeToString([]byte("{foo: bar}")) c := GenericClaims{} err := parseClaims(payload, &c) if err == nil { t.Fatal("should have failed bad json") } } func TestBadPublicKeyDecodeGeneric(t *testing.T) { c := &GenericClaims{} c.Issuer = "foo" if ok := c.verify("foo", []byte("bar")); ok { t.Fatal("Should have failed to verify") } } func TestBadSig(t *testing.T) { opk := createOperatorNKey(t) kp := createAccountNKey(t) claims := NewGenericClaims(publicKey(kp, t)) claims.Data["foo"] = "bar" // serialize the claim to a JWT token token := encode(claims, opk, t) tokens := strings.Split(token, ".") badToken := fmt.Sprintf("%s.%s.=hello=", tokens[0], tokens[1]) _, err := DecodeGeneric(badToken) if err == nil { t.Fatal("should have failed to base64 decode signature") } } func TestClaimsStringIsJSON(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) // add a bunch of claims claims.Data["foo"] = "bar" claims2 := NewGenericClaims(publicKey(akp, t)) if json.Unmarshal([]byte(claims.String()), claims2) != nil { t.Fatal("failed to unmarshal claims") } if claims2.Data["foo"] != "bar" { t.Fatalf("Failed to decode expected claim from String representation: %q", claims.String()) } } func TestDoEncodeNilHeader(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) _, err := claims.doEncode(nil, nil, claims, nil) if err == nil { t.Fatal("should have failed to encode") } if err.Error() != "header is required" { t.Fatalf("unexpected error on encode: %v", err) } } func TestDoEncodeNilKeyPair(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) _, err := claims.doEncode(&Header{}, nil, claims, nil) if err == nil { t.Fatal("should have failed to encode") } if err.Error() != "keypair is required" { t.Fatalf("unexpected error on encode: %v", err) } } // if this fails, the URL decoder was changed and JWTs will flap func TestUsingURLDecoder(t *testing.T) { token := "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJGQ1lZRjJLR0EzQTZHTlZQR0pIVjNUSExYR1VZWkFUREZLV1JTT1czUUo1T0k3QlJST0ZRIiwiaWF0IjoxNTQzOTQzNjc1LCJpc3MiOiJBQ1NKWkhOWlI0QUFUVE1KNzdUV1JONUJHVUZFWFhUS0gzWEtGTldDRkFCVzJRWldOUTRDQkhRRSIsInN1YiI6IkFEVEFHWVZYRkpPRENRM0g0VUZQQU43R1dXWk1BVU9FTTJMMkRWQkFWVFdLM01TU0xUS1JUTzVGIiwidHlwZSI6ImFjdGl2YXRpb24iLCJuYXRzIjp7InN1YmplY3QiOiJmb28iLCJ0eXBlIjoic2VydmljZSJ9fQ.HCZTCF-7wolS3Wjx3swQWMkoDhoo_4gp9EsuM5diJfZrH8s6NTpO0iT7_fKZm7dNDeEoqjwU--3ebp8j-Mm_Aw" ac, err := DecodeActivationClaims(token) if err != nil { t.Fatal("shouldn't have failed to decode", err) } if ac == nil { t.Fatal("should have returned activation") } } jwt-2.7.3/v2/decoder_user.go000066400000000000000000000041361472706253100156630ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" ) type v1User struct { Permissions Limits BearerToken bool `json:"bearer_token,omitempty"` // Limit values deprecated inv v2 Max int64 `json:"max,omitempty"` } type v1UserClaimsDataDeletedFields struct { v1ClaimsDataDeletedFields IssuerAccount string `json:"issuer_account,omitempty"` } type v1UserClaims struct { ClaimsData v1UserClaimsDataDeletedFields v1User `json:"nats,omitempty"` } func loadUser(data []byte, version int) (*UserClaims, error) { switch version { case 1: var v1a v1UserClaims v1a.Limits = Limits{NatsLimits: NatsLimits{NoLimit, NoLimit, NoLimit}} v1a.Max = NoLimit if err := json.Unmarshal(data, &v1a); err != nil { return nil, err } return v1a.Migrate() case 2: var v2a UserClaims if err := json.Unmarshal(data, &v2a); err != nil { return nil, err } return &v2a, nil default: return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) } } func (oa v1UserClaims) Migrate() (*UserClaims, error) { return oa.migrateV1() } func (oa v1UserClaims) migrateV1() (*UserClaims, error) { var u UserClaims // copy the base claim u.ClaimsData = oa.ClaimsData // move the moved fields u.User.Type = oa.v1ClaimsDataDeletedFields.Type u.User.Tags = oa.v1ClaimsDataDeletedFields.Tags u.User.IssuerAccount = oa.IssuerAccount // copy the user data u.User.Permissions = oa.v1User.Permissions u.User.Limits = oa.v1User.Limits u.User.BearerToken = oa.v1User.BearerToken u.Version = 1 return &u, nil } jwt-2.7.3/v2/example_test.go000066400000000000000000000116311472706253100157100ustar00rootroot00000000000000package jwt import ( "fmt" "os" "path" "testing" jwt "github.com/nats-io/jwt/v2/v1compat" "github.com/nats-io/nkeys" ) func TestExample(t *testing.T) { // create an operator key pair (private key) okp, err := nkeys.CreateOperator() if err != nil { t.Fatal(err) } // extract the public key opk, err := okp.PublicKey() if err != nil { t.Fatal(err) } // create an operator claim using the public key for the identifier oc := jwt.NewOperatorClaims(opk) oc.Name = "O" // add an operator signing key to sign accounts oskp, err := nkeys.CreateOperator() if err != nil { t.Fatal(err) } // get the public key for the signing key ospk, err := oskp.PublicKey() if err != nil { t.Fatal(err) } // add the signing key to the operator - this makes any account // issued by the signing key to be valid for the operator oc.SigningKeys.Add(ospk) // self-sign the operator JWT - the operator trusts itself operatorJWT, err := oc.Encode(okp) if err != nil { t.Fatal(err) } // create an account keypair akp, err := nkeys.CreateAccount() if err != nil { t.Fatal(err) } // extract the public key for the account apk, err := akp.PublicKey() if err != nil { t.Fatal(err) } // create the claim for the account using the public key of the account ac := jwt.NewAccountClaims(apk) ac.Name = "A" // create a signing key that we can use for issuing users askp, err := nkeys.CreateAccount() if err != nil { t.Fatal(err) } // extract the public key aspk, err := askp.PublicKey() if err != nil { t.Fatal(err) } // add the signing key (public) to the account ac.SigningKeys.Add(aspk) // now we could encode an issue the account using the operator // key that we generated above, but this will illustrate that // the account could be self-signed, and given to the operator // who can then re-sign it accountJWT, err := ac.Encode(akp) if err != nil { t.Fatal(err) } // the operator would decode the provided token, if the token // is not self-signed or signed by an operator or tampered with // the decoding would fail ac, err = jwt.DecodeAccountClaims(accountJWT) if err != nil { t.Fatal(err) } // here the operator is going to use its private signing key to // re-issue the account accountJWT, err = ac.Encode(oskp) if err != nil { t.Fatal(err) } // 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 ukp, err := nkeys.CreateUser() if err != nil { t.Fatal(err) } upk, err := ukp.PublicKey() if err != nil { t.Fatal(err) } 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.IssuerAccount = apk userJwt, err := uc.Encode(askp) if err != nil { t.Fatal(err) } // the seed is a version of the keypair that is stored as text useed, err := ukp.Seed() if err != nil { t.Fatal(err) } // generate a creds formatted file that can be used by a NATS client creds, err := jwt.FormatUserConfig(userJwt, useed) if err != nil { t.Fatal(err) } // now we are going to put it together into something that can be run // we create a directory to store the server configuration, the creds // file and a small go program that uses the creds file dir, err := os.MkdirTemp(os.TempDir(), "jwt_example") if err != nil { t.Fatal(err) } // print where we generated the file t.Logf("generated example %s", dir) t.Log("to run this example:") t.Logf("> cd %s", dir) t.Log("> go mod init example") t.Log("> go mod tidy") t.Logf("> nats-server -c %s/resolver.conf &", dir) t.Log("> go run main.go") // we are generating a memory resolver server configuration // it lists the operator and all account jwts the server should // know about resolver := fmt.Sprintf(`operator: %s resolver: MEMORY resolver_preload: { %s: %s } `, operatorJWT, apk, accountJWT) if err := os.WriteFile(path.Join(dir, "resolver.conf"), []byte(resolver), 0644); err != nil { t.Fatal(err) } // store the creds credsPath := path.Join(dir, "u.creds") if err := os.WriteFile(credsPath, creds, 0644); err != nil { t.Fatal(err) } // here we generate as small go program that connects using the creds file // subscribes, and publishes a message connect := fmt.Sprintf(` package main import ( "fmt" "sync" "github.com/nats-io/nats.go" ) func main() { var wg sync.WaitGroup wg.Add(1) nc, err := nats.Connect(nats.DefaultURL, nats.UserCredentials(%q)) if err != nil { panic(err) } nc.Subscribe("hello.world", func(m *nats.Msg) { fmt.Println(m.Subject) wg.Done() }) nc.Publish("hello.world", []byte("hello")) nc.Flush() wg.Wait() nc.Close() } `, credsPath) if err := os.WriteFile(path.Join(dir, "main.go"), []byte(connect), 0644); err != nil { t.Fatal(err) } } jwt-2.7.3/v2/exports.go000066400000000000000000000222321472706253100147210ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "strings" "time" ) // ResponseType is used to store an export response type type ResponseType string const ( // ResponseTypeSingleton is used for a service that sends a single response only ResponseTypeSingleton = "Singleton" // ResponseTypeStream is used for a service that will send multiple responses ResponseTypeStream = "Stream" // ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream) ResponseTypeChunked = "Chunked" ) // ServiceLatency is used when observing and exported service for // latency measurements. // Sampling 1-100, represents sampling rate, defaults to 100. // Results is the subject where the latency metrics are published. // A metric will be defined by the nats-server's ServiceLatency. Time durations // are in nanoseconds. // see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524 // e.g. // // { // "app": "dlc22", // "start": "2019-09-16T21:46:23.636869585-07:00", // "svc": 219732, // "nats": { // "req": 320415, // "resp": 228268, // "sys": 0 // }, // "total": 768415 // } type ServiceLatency struct { Sampling SamplingRate `json:"sampling"` Results Subject `json:"results"` } type SamplingRate int const Headers = SamplingRate(0) // MarshalJSON marshals the field as "headers" or percentages func (r *SamplingRate) MarshalJSON() ([]byte, error) { sr := *r if sr == 0 { return []byte(`"headers"`), nil } if sr >= 1 && sr <= 100 { return []byte(fmt.Sprintf("%d", sr)), nil } return nil, fmt.Errorf("unknown sampling rate") } // UnmarshalJSON unmashals numbers as percentages or "headers" func (t *SamplingRate) UnmarshalJSON(b []byte) error { if len(b) == 0 { return fmt.Errorf("empty sampling rate") } if strings.ToLower(string(b)) == `"headers"` { *t = Headers return nil } var j int err := json.Unmarshal(b, &j) if err != nil { return err } *t = SamplingRate(j) return nil } func (sl *ServiceLatency) Validate(vr *ValidationResults) { if sl.Sampling != 0 { if sl.Sampling < 1 || sl.Sampling > 100 { vr.AddError("sampling percentage needs to be between 1-100") } } sl.Results.Validate(vr) if sl.Results.HasWildCards() { vr.AddError("results subject can not contain wildcards") } } // Export represents a single export type Export struct { Name string `json:"name,omitempty"` Subject Subject `json:"subject,omitempty"` Type ExportType `json:"type,omitempty"` TokenReq bool `json:"token_req,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` ResponseType ResponseType `json:"response_type,omitempty"` ResponseThreshold time.Duration `json:"response_threshold,omitempty"` Latency *ServiceLatency `json:"service_latency,omitempty"` AccountTokenPosition uint `json:"account_token_position,omitempty"` Advertise bool `json:"advertise,omitempty"` AllowTrace bool `json:"allow_trace,omitempty"` Info } // IsService returns true if an export is for a service func (e *Export) IsService() bool { return e.Type == Service } // IsStream returns true if an export is for a stream func (e *Export) IsStream() bool { return e.Type == Stream } // IsSingleResponse returns true if an export has a single response // or no response type is set, also checks that the type is service func (e *Export) IsSingleResponse() bool { return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "") } // IsChunkedResponse returns true if an export has a chunked response func (e *Export) IsChunkedResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeChunked } // IsStreamResponse returns true if an export has a chunked response func (e *Export) IsStreamResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeStream } // Validate appends validation issues to the passed in results list func (e *Export) Validate(vr *ValidationResults) { if e == nil { vr.AddError("null export is not allowed") return } if !e.IsService() && !e.IsStream() { vr.AddError("invalid export type: %q", e.Type) } if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() { vr.AddError("invalid response type for service: %q", e.ResponseType) } if e.IsStream() { if e.ResponseType != "" { vr.AddError("invalid response type for stream: %q", e.ResponseType) } if e.AllowTrace { vr.AddError("AllowTrace only valid for service export") } } if e.Latency != nil { if !e.IsService() { vr.AddError("latency tracking only permitted for services") } e.Latency.Validate(vr) } if e.ResponseThreshold.Nanoseconds() < 0 { vr.AddError("negative response threshold is invalid") } if e.ResponseThreshold.Nanoseconds() > 0 && !e.IsService() { vr.AddError("response threshold only valid for services") } e.Subject.Validate(vr) if e.AccountTokenPosition > 0 { if !e.Subject.HasWildCards() { vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject) } else { subj := string(e.Subject) token := strings.Split(subj, ".") tkCnt := uint(len(token)) if e.AccountTokenPosition > tkCnt { vr.AddError("Account Token Position %d exceeds length of subject '%s'", e.AccountTokenPosition, e.Subject) } else if tk := token[e.AccountTokenPosition-1]; tk != "*" { vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s", e.AccountTokenPosition, tk, e.Subject) } } } e.Info.Validate(vr) } // Revoke enters a revocation by publickey using time.Now(). func (e *Export) Revoke(pubKey string) { e.RevokeAt(pubKey, time.Now()) } // RevokeAt enters a revocation by publickey and timestamp into this export // If there is already a revocation for this public key that is newer, it is kept. func (e *Export) RevokeAt(pubKey string, timestamp time.Time) { if e.Revocations == nil { e.Revocations = RevocationList{} } e.Revocations.Revoke(pubKey, timestamp) } // ClearRevocation removes any revocation for the public key func (e *Export) ClearRevocation(pubKey string) { e.Revocations.ClearRevocation(pubKey) } // isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in. // Generally this method is called with the subject and issue time of the jwt to be tested. // DO NOT pass time.Now(), it will not produce a stable/expected response. func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool { return e.Revocations.IsRevoked(pubKey, claimIssuedAt) } // IsClaimRevoked checks if the activation revoked the claim passed in. // Invalid claims (nil, no Subject or IssuedAt) will return true. func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool { if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { return true } return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) } // Exports is a slice of exports type Exports []*Export // Add appends exports to the list func (e *Exports) Add(i ...*Export) { *e = append(*e, i...) } func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { m := make(map[string]string) for i, ns := range subjects { for j, s := range subjects { if i == j { continue } if ns.IsContainedIn(s) { str := string(s) _, ok := m[str] if !ok { m[str] = string(ns) } } } } if len(m) != 0 { for k, v := range m { var vi ValidationIssue vi.Blocking = true vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v) vr.Add(&vi) } } } // Validate calls validate on all of the exports func (e *Exports) Validate(vr *ValidationResults) { var serviceSubjects []Subject var streamSubjects []Subject for _, v := range *e { if v == nil { vr.AddError("null export is not allowed") continue } if v.IsService() { serviceSubjects = append(serviceSubjects, v.Subject) } else { streamSubjects = append(streamSubjects, v.Subject) } v.Validate(vr) } isContainedIn(Service, serviceSubjects, vr) isContainedIn(Stream, streamSubjects, vr) } // HasExportContainingSubject checks if the export list has an export with the provided subject func (e *Exports) HasExportContainingSubject(subject Subject) bool { for _, s := range *e { if subject.IsContainedIn(s.Subject) { return true } } return false } func (e Exports) Len() int { return len(e) } func (e Exports) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (e Exports) Less(i, j int) bool { return e[i].Subject < e[j].Subject } jwt-2.7.3/v2/exports_test.go000066400000000000000000000277551472706253100157770ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "sort" "strings" "testing" "time" "github.com/nats-io/nkeys" ) func TestSimpleExportValidation(t *testing.T) { e := &Export{Subject: "foo", Type: Stream, Info: Info{InfoURL: "http://localhost/foo/bar", Description: "description"}} vr := CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("simple export should validate cleanly") } e.Type = Service vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("simple export should validate cleanly") } } func TestResponseTypeValidation(t *testing.T) { e := &Export{Subject: "foo", Type: Stream, ResponseType: ResponseTypeSingleton} vr := CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("response type on stream should have an validation issue") } if e.IsSingleResponse() { t.Errorf("response type should always fail for stream") } e.Type = Service vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be single") } e.ResponseType = ResponseTypeChunked vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if e.IsSingleResponse() || !e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be chunk") } e.ResponseType = ResponseTypeStream vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if e.IsSingleResponse() || e.IsChunkedResponse() || !e.IsStreamResponse() { t.Errorf("response type should be stream") } e.ResponseType = "" vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be single") } e.ResponseType = "bad" vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("response type should match available options") } if e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be bad") } } func TestInvalidExportType(t *testing.T) { i := &Export{Subject: "foo", Type: Unknown} vr := CreateValidationResults() i.Validate(vr) if vr.IsEmpty() { t.Errorf("export with bad type should not validate cleanly") } if !vr.IsBlocking(true) { t.Errorf("invalid type is blocking") } } func TestInvalidExportInfo(t *testing.T) { e := &Export{Subject: "foo", Type: Stream, Info: Info{InfoURL: "/bad"}} vr := CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("export info should not validate cleanly") } if !vr.IsBlocking(true) { t.Errorf("invalid info needs to be blocking") } } func TestOverlappingExports(t *testing.T) { i := &Export{Subject: "bar.foo", Type: Stream} i2 := &Export{Subject: "bar.*", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 1 { t.Errorf("export has overlapping subjects") } } func TestDifferentExportTypes_OverlapOK(t *testing.T) { i := &Export{Subject: "bar.foo", Type: Service} i2 := &Export{Subject: "bar.*", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 0 { t.Errorf("should allow overlaps on different export kind") } } func TestDifferentExportTypes_SameSubjectOK(t *testing.T) { i := &Export{Subject: "bar", Type: Service} i2 := &Export{Subject: "bar", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 0 { t.Errorf("should allow overlaps on different export kind") } } func TestSameExportType_SameSubject(t *testing.T) { i := &Export{Subject: "bar", Type: Service} i2 := &Export{Subject: "bar", Type: Service} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 1 { t.Errorf("should not allow same subject on same export kind") } } func TestExportRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) e := &Export{Subject: "foo", Type: Stream} account.Exports.Add(e) ikp := createAccountNKey(t) pubKey := publicKey(ikp, t) ac := NewActivationClaims(pubKey) ac.IssuerAccount = apk ac.Name = "foo" ac.Activation.ImportSubject = "foo" ac.Activation.ImportType = Stream aJwt, _ := ac.Encode(akp) ac, err := DecodeActivationClaims(aJwt) if err != nil { t.Errorf("Failed to decode activation claim: %v", err) } now := time.Now() // test that clear is safe before we add any e.ClearRevocation(pubKey) if e.isRevoked(pubKey, now) { t.Errorf("no revocation was added so is revoked should be false") } e.RevokeAt(pubKey, now.Add(time.Second*100)) if !e.isRevoked(pubKey, now) { t.Errorf("revocation should hold when timestamp is in the future") } if e.isRevoked(pubKey, now.Add(time.Second*150)) { t.Errorf("revocation should time out") } e.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in if !e.isRevoked(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should hold, 100 > 50") } encoded, _ := account.Encode(akp) decoded, _ := DecodeAccountClaims(encoded) if !decoded.Exports[0].isRevoked(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should last across encoding") } e.ClearRevocation(pubKey) if e.IsClaimRevoked(ac) { t.Errorf("revocations should be cleared") } e.RevokeAt(pubKey, now) if !e.IsClaimRevoked(ac) { t.Errorf("revocation be true we revoked in the future") } } func TestExportTrackLatency(t *testing.T) { e := &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} vr := CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("Expected to validate with simple tracking") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: Headers, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("Headers must not need to ") } e = &Export{Subject: "foo", Type: Stream} e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("adding latency tracking to a stream should have an validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: -1, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Sampling <1 should have a validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 122, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Sampling >100 should have a validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 22, Results: "results.*"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Results subject needs to be valid publish subject") } } func TestExportTrackHeader(t *testing.T) { akp, err := nkeys.CreateAccount() AssertNoError(err, t) apk, err := akp.PublicKey() AssertNoError(err, t) ac := NewAccountClaims(apk) e := &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: Headers, Results: "results"} ac.Exports.Add(e) theJWT, err := ac.Encode(akp) AssertNoError(err, t) ac2, err := DecodeAccountClaims(theJWT) AssertNoError(err, t) if *(ac2.Exports[0].Latency) != *e.Latency { t.Errorf("Headers need to de serialize as headers") } } func TestExport_Sorting(t *testing.T) { var exports Exports exports.Add(&Export{Subject: "x", Type: Service}) exports.Add(&Export{Subject: "z", Type: Service}) exports.Add(&Export{Subject: "y", Type: Service}) if exports[0] == nil || exports[0].Subject != "x" { t.Fatal("added export not in expected order") } sort.Sort(exports) if exports[0].Subject != "x" && exports[1].Subject != "y" && exports[2].Subject != "z" { t.Fatal("exports not sorted") } } func TestExportAccountTokenPos(t *testing.T) { okp := createOperatorNKey(t) akp := createAccountNKey(t) apk := publicKey(akp, t) tbl := map[Subject]uint{ "*": 1, "foo.*": 2, "foo.*.bar.*": 2, "foo.*.bar.>": 2, "*.*.*.>": 2, "*.*.>": 1, } for k, v := range tbl { t.Run(string(k), func(t *testing.T) { account := NewAccountClaims(apk) //account.Limits = OperatorLimits{} account.Exports = append(account.Exports, &Export{Type: Stream, Subject: k, AccountTokenPosition: v}) actJwt := encode(account, okp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) vr := &ValidationResults{} account2.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("validation issues", *vr) } }) } } func TestExportAccountTokenPosFail(t *testing.T) { okp := createOperatorNKey(t) akp := createAccountNKey(t) apk := publicKey(akp, t) tbl := map[Subject]uint{ ">": 5, "foo.>": 2, "bar.>": 1, "*": 5, "*.*": 5, "bar": 1, "foo.bar": 2, "foo.*.bar": 3, "*.>": 3, "*.*.>": 3, "foo.*x.bar": 2, "foo.x*.bar": 2, } for k, v := range tbl { t.Run(string(k), func(t *testing.T) { account := NewAccountClaims(apk) //account.Limits = OperatorLimits{} account.Exports = append(account.Exports, &Export{Type: Stream, Subject: k, AccountTokenPosition: v}) actJwt := encode(account, okp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) vr := &ValidationResults{} account2.Validate(vr) if len(vr.Issues) != 1 { t.Fatal("validation issue expected", *vr) } }) } } func TestExport_ResponseThreshold(t *testing.T) { var exports Exports exports.Add(&Export{Subject: "x", Type: Service, ResponseThreshold: time.Second}) vr := ValidationResults{} exports.Validate(&vr) if !vr.IsEmpty() { t.Fatal("expected this to pass") } exports = Exports{} exports.Add(&Export{Subject: "x", Type: Stream, ResponseThreshold: time.Second}) vr = ValidationResults{} exports.Validate(&vr) if vr.IsEmpty() { t.Fatal("expected this to fail due to type") } exports = Exports{} exports.Add(&Export{Subject: "x", Type: Service, ResponseThreshold: -1 * time.Second}) vr = ValidationResults{} exports.Validate(&vr) if vr.IsEmpty() { t.Fatal("expected this to fail due to negative duration") } } func TestExportAllowTrace(t *testing.T) { // AllowTrace is only applicable to ServiceExport e := &Export{Subject: "foo", Type: Stream, AllowTrace: true} vr := CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Fatalf("AllowTrace on stream should have an validation issue") } issue := vr.Issues[0] if !strings.Contains(issue.Description, "AllowTrace only valid for service export") { t.Fatalf("AllowTrace should be valid only for service export, got %q", issue.Description) } e.Type = Service vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Fatalf("validation should have been ok, got %+v", vr.Issues) } } jwt-2.7.3/v2/genericlaims.go000066400000000000000000000075221472706253100156640ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "encoding/json" "errors" "strings" "github.com/nats-io/nkeys" ) // GenericClaims can be used to read a JWT as a map for any non-generic fields type GenericClaims struct { ClaimsData Data map[string]interface{} `json:"nats,omitempty"` } // NewGenericClaims creates a map-based Claims func NewGenericClaims(subject string) *GenericClaims { if subject == "" { return nil } c := GenericClaims{} c.Subject = subject c.Data = make(map[string]interface{}) return &c } // DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map func DecodeGeneric(token string) (*GenericClaims, error) { // must have 3 chunks chunks := strings.Split(token, ".") if len(chunks) != 3 { return nil, errors.New("expected 3 chunks") } // header header, err := parseHeaders(chunks[0]) if err != nil { return nil, err } // claim data, err := decodeString(chunks[1]) if err != nil { return nil, err } gc := struct { GenericClaims GenericFields }{} if err := json.Unmarshal(data, &gc); err != nil { return nil, err } // sig sig, err := decodeString(chunks[2]) if err != nil { return nil, err } if header.Algorithm == AlgorithmNkeyOld { if !gc.verify(chunks[1], sig) { return nil, errors.New("claim failed V1 signature verification") } if tp := gc.GenericFields.Type; tp != "" { // the conversion needs to be from a string because // on custom types the type is not going to be one of // the constants gc.GenericClaims.Data["type"] = string(tp) } if tp := gc.GenericFields.Tags; len(tp) != 0 { gc.GenericClaims.Data["tags"] = tp } } else { if !gc.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) { return nil, errors.New("claim failed V2 signature verification") } } return &gc.GenericClaims, nil } // Claims returns the standard part of the generic claim func (gc *GenericClaims) Claims() *ClaimsData { return &gc.ClaimsData } // Payload returns the custom part of the claims data func (gc *GenericClaims) Payload() interface{} { return &gc.Data } // Encode takes a generic claims and creates a JWT string func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) { return gc.ClaimsData.encode(pair, gc, nil) } func (gc *GenericClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { return gc.ClaimsData.encode(pair, gc, fn) } // Validate checks the generic part of the claims data func (gc *GenericClaims) Validate(vr *ValidationResults) { gc.ClaimsData.Validate(vr) } func (gc *GenericClaims) String() string { return gc.ClaimsData.String(gc) } // ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte { return nil } func (gc *GenericClaims) ClaimType() ClaimType { v, ok := gc.Data["type"] if !ok { v, ok = gc.Data["nats"] if ok { m, ok := v.(map[string]interface{}) if ok { v = m["type"] } } } switch ct := v.(type) { case string: if IsGenericClaimType(ct) { return GenericClaim } return ClaimType(ct) case ClaimType: return ct default: return "" } } func (gc *GenericClaims) updateVersion() { if gc.Data != nil { // store as float as that is what decoding with json does too gc.Data["version"] = float64(libVersion) } } jwt-2.7.3/v2/go.mod000066400000000000000000000004651472706253100140000ustar00rootroot00000000000000module github.com/nats-io/jwt/v2 go 1.22 require github.com/nats-io/nkeys v0.4.9 retract ( v2.7.1 // contains retractions only v2.7.0 // includes case insensitive changes to tags that break jetstream placement ) require ( golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect ) jwt-2.7.3/v2/go.sum000066400000000000000000000007371472706253100140270ustar00rootroot00000000000000github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= jwt-2.7.3/v2/header.go000066400000000000000000000041141472706253100144440ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "strings" ) const ( // Version is semantic version. Version = "2.4.0" // 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 TokenTypeJwt = "JWT" // AlgorithmNkey is the algorithm supported by JWT tokens // encoded and decoded by this library AlgorithmNkeyOld = "ed25519" AlgorithmNkey = AlgorithmNkeyOld + "-nkey" ) // Header is a JWT Jose Header type Header struct { Type string `json:"typ"` Algorithm string `json:"alg"` } // Parses a header JWT token func parseHeaders(s string) (*Header, error) { h, err := decodeString(s) if err != nil { return nil, err } header := Header{} if err := json.Unmarshal(h, &header); err != nil { return nil, err } if err := header.Valid(); err != nil { return nil, err } return &header, nil } // Valid validates the Header. It returns nil if the Header is // a JWT header, and the algorithm used is the NKEY algorithm. func (h *Header) Valid() error { if TokenTypeJwt != strings.ToUpper(h.Type) { return fmt.Errorf("not supported type %q", h.Type) } alg := strings.ToLower(h.Algorithm) if !strings.HasPrefix(alg, AlgorithmNkeyOld) { return fmt.Errorf("unexpected %q algorithm", h.Algorithm) } if AlgorithmNkeyOld != alg && AlgorithmNkey != alg { return fmt.Errorf("unexpected %q algorithm", h.Algorithm) } return nil } jwt-2.7.3/v2/imports.go000066400000000000000000000126251472706253100147170ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt // Import describes a mapping from another account into this one type Import struct { Name string `json:"name,omitempty"` // 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). Subject Subject `json:"subject,omitempty"` Account string `json:"account,omitempty"` Token string `json:"token,omitempty"` // Deprecated: use LocalSubject instead // 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. To Subject `json:"to,omitempty"` // 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 Subject. // If the value of Subject ends in > then LocalSubject needs to end in > as well. // LocalSubject can contain $ wildcard references where number references the nth wildcard in Subject. // The sum of wildcard reference and * tokens needs to match the number of * token in Subject. LocalSubject RenamingSubject `json:"local_subject,omitempty"` Type ExportType `json:"type,omitempty"` Share bool `json:"share,omitempty"` AllowTrace bool `json:"allow_trace,omitempty"` } // IsService returns true if the import is of type service func (i *Import) IsService() bool { return i.Type == Service } // IsStream returns true if the import is of type stream func (i *Import) IsStream() bool { return i.Type == Stream } // Returns the value of To without triggering the deprecation warning for a read func (i *Import) GetTo() string { return string(i.To) } // Validate checks if an import is valid for the wrapping account func (i *Import) Validate(actPubKey string, vr *ValidationResults) { if i == nil { vr.AddError("null import is not allowed") return } if !i.IsService() && !i.IsStream() { vr.AddError("invalid import type: %q", i.Type) } if i.IsService() && i.AllowTrace { vr.AddError("AllowTrace only valid for stream import") } if i.Account == "" { vr.AddError("account to import from is not specified") } if i.GetTo() != "" { vr.AddWarning("the field to has been deprecated (use LocalSubject instead)") } i.Subject.Validate(vr) if i.LocalSubject != "" { i.LocalSubject.Validate(i.Subject, vr) if i.To != "" { vr.AddError("Local Subject replaces To") } } if i.Share && !i.IsService() { vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject) } var act *ActivationClaims if i.Token != "" { var err error act, err = DecodeActivationClaims(i.Token) if err != nil { vr.AddError("import %q contains an invalid activation token", i.Subject) } } if act != nil { if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { vr.AddError("activation token doesn't match account for import %q", i.Subject) } if act.ClaimsData.Subject != actPubKey { vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) } if act.ImportType != i.Type { vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) } act.validateWithTimeChecks(vr, false) subj := i.Subject if i.IsService() && i.To != "" { subj = i.To } if !subj.IsContainedIn(act.ImportSubject) { vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) } } } // Imports is a list of import structs type Imports []*Import // Validate checks if an import is valid for the wrapping account func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) { toSet := make(map[Subject]struct{}, len(*i)) for _, v := range *i { if v == nil { vr.AddError("null import is not allowed") continue } if v.Type == Service { sub := v.To if sub == "" { sub = v.LocalSubject.ToSubject() } if sub == "" { sub = v.Subject } for k := range toSet { if sub.IsContainedIn(k) || k.IsContainedIn(sub) { vr.AddError("overlapping subject namespace for %q and %q", sub, k) } } if _, ok := toSet[sub]; ok { vr.AddError("overlapping subject namespace for %q", v.To) } toSet[sub] = struct{}{} } v.Validate(acctPubKey, vr) } } // Add is a simple way to add imports func (i *Imports) Add(a ...*Import) { *i = append(*i, a...) } func (i Imports) Len() int { return len(i) } func (i Imports) Swap(j, k int) { i[j], i[k] = i[k], i[j] } func (i Imports) Less(j, k int) bool { return i[j].Subject < i[k].Subject } jwt-2.7.3/v2/imports_test.go000066400000000000000000000336401472706253100157560ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt import ( "sort" "strings" "testing" "time" ) func TestImportValidation(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream actJWT := encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with token should be valid") } } func TestImportValidationExpiredToken(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} // test success, expiration is not checked activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(-time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream i.Token = encode(activation, ak2, t) vr := CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("Expired token should not trigger a validation issue") } // test failure, different issuer ak3 := createAccountNKey(t) activation = NewActivationClaims(akp) activation.Expires = time.Now().Add(-time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream i.Token = encode(activation, ak3, t) vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("Issuer mismatch must trigger a validation issue") } } func TestImportValidationDifferentAccount(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) otherAccount := publicKey(createAccountNKey(t), t) i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} activation := NewActivationClaims(otherAccount) activation.Expires = time.Now().Add(-time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream i.Token = encode(activation, ak2, t) vr := CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() || !vr.IsBlocking(false) { t.Errorf("Expired import needs to result in a time check error") } } func TestImportValidationSigningKey(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) ak2Sk := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Stream} activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream activation.IssuerAccount = akp2 i.Token = encode(activation, ak2Sk, t) vr := CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("Expired import needs to not result in an error") } } func TestInvalidImportType(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Unknown} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Errorf("imports without token or url should warn the caller") } if !vr.IsBlocking(true) { t.Errorf("invalid type is blocking") } } func TestInvalidImportToken(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, Token: "bad token", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Errorf("imports with a bad token or url should cause an error") } if !vr.IsBlocking(false) { t.Errorf("invalid type should be blocking") } } func TestInvalidImportURL(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, Token: "foo://bad-token-url", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Errorf("imports with a bad token or url should warn the caller") } if !vr.IsBlocking(true) { t.Errorf("invalid type should be blocking") } } func TestInvalidImportTokenValuesValidation(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, LocalSubject: "bar", Type: Service} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } i.Type = Service vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Service actJWT := encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with token should be valid") } actJWT = encode(activation, ak, t) // wrong issuer i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with wrong issuer") } activation.Subject = akp2 // wrong subject actJWT = encode(activation, ak2, t) // right issuer i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with wrong issuer") } } func TestMissingAccountInImport(t *testing.T) { i := &Import{Subject: "foo", LocalSubject: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if len(vr.Issues) != 1 { t.Errorf("expected only one issue") } if !vr.IsBlocking(true) { t.Errorf("Missing Account is blocking") } } func TestServiceImportWithWildcard(t *testing.T) { i := &Import{Subject: "foo.>", Account: publicKey(createAccountNKey(t), t), LocalSubject: "bar.>", Type: Service} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("expected no issue") } i.Subject = ">" vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("expected no issue") } } func TestStreamImportWithWildcardPrefix(t *testing.T) { i := &Import{Subject: "foo.>", Account: publicKey(createAccountNKey(t), t), LocalSubject: "bar.>", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("expected no issue") } i.Subject = ">" vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("expected no issue") } } func TestStreamImportInformationSharing(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) // broken import share won't work with streams i := &Import{Subject: "foo", Account: akp, Type: Stream, Share: true} vr := CreateValidationResults() i.Validate("", vr) if len(vr.Issues) != 1 { t.Errorf("should have registered 1 issues with this import, got %d", len(vr.Issues)) } if !vr.IsBlocking(true) { t.Fatalf("issue is expected to be blocking") } // import share will work with service i.Type = Service vr = CreateValidationResults() i.Validate("", vr) if len(vr.Issues) != 0 { t.Errorf("should have registered 0 issues with this import, got %d", len(vr.Issues)) } } func TestImportsValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, LocalSubject: "bar", Type: Stream} i2 := &Import{Subject: "foo.*", Account: akp, LocalSubject: "bar.*", Type: Service} imports := &Imports{} imports.Add(i, i2) vr := CreateValidationResults() imports.Validate("", vr) if !vr.IsEmpty() { t.Errorf("no issues expected") } } func TestImportsLocalSubjectExclusiveTo(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, LocalSubject: "bar", Type: Stream} i2 := &Import{Subject: "foo", Account: akp, LocalSubject: "bar", Type: Service} imports := &Imports{} imports.Add(i, i2) vr := CreateValidationResults() imports.Validate("", vr) if !vr.IsEmpty() { t.Errorf("no issues expected") } i.To = "bar" i2.To = "bar" imports = &Imports{} imports.Add(i, i2) vr = CreateValidationResults() imports.Validate("", vr) if vr.IsEmpty() { t.Errorf("issues expected") } if !vr.IsBlocking(false) { t.Errorf("issues expected to be blocking") } } func TestImportsLocalSubjectVariants(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) imports := &Imports{} imports.Add( &Import{Subject: "foo.*.bar.*.>", Account: akp, LocalSubject: "my.$2.$1.>", Type: Stream}, &Import{Subject: "baz.*.bar.*.>", Account: akp, LocalSubject: "bar.*.*.>", Type: Service}, &Import{Subject: "baz.*", Account: akp, LocalSubject: "my.$1", Type: Stream}, &Import{Subject: "bar.*", Account: akp, LocalSubject: "baz.*", Type: Service}, &Import{Subject: "biz.*.*.*", Account: akp, LocalSubject: "buz.*.*.*", Type: Service}) vr := CreateValidationResults() imports.Validate("", vr) if !vr.IsEmpty() { t.Errorf("no issues expected") } } func TestImportSubjectValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "one.*" activation.ImportType = Stream ak2 := createAccountNKey(t) akp2 := publicKey(ak2, t) i := &Import{Subject: "one.two", Account: akp2, LocalSubject: "bar", Type: Stream} i.Token = encode(activation, ak2, t) vr := CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Log(vr.Issues[0].Description) t.Errorf("imports with valid contains subject should be valid") } activation.ImportSubject = "two" activation.ImportType = Stream i.Token = encode(activation, ak2, t) vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with non-contains subject should be not valid") } activation.ImportSubject = ">" activation.ImportType = Stream i.Token = encode(activation, ak2, t) vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with valid contains subject should be valid") } } func TestImportServiceDoubleToSubjectsValidation(t *testing.T) { akp := createAccountNKey(t) akp2 := createAccountNKey(t) apk := publicKey(akp, t) apk2 := publicKey(akp2, t) account := NewAccountClaims(apk) i := &Import{Subject: "one.two", Account: apk2, To: "foo.bar", Type: Service} account.Imports.Add(i) vr := CreateValidationResults() account.Validate(vr) if vr.IsBlocking(true) { t.Fatalf("Expected no blocking validation errors") } i2 := &Import{Subject: "two.three", Account: apk2, To: "foo.bar", Type: Service} account.Imports.Add(i2) vr = CreateValidationResults() account.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("Expected multiple import 'to' subjects to produce an error") } } func TestWildcard(t *testing.T) { account := NewAccountClaims(publicKey(createAccountNKey(t), t)) i := &Import{Subject: ">", Account: publicKey(createAccountNKey(t), t), To: "foo.bar", Type: Service} account.Imports.Add(i) vr := CreateValidationResults() account.Validate(vr) if vr.IsBlocking(true) { t.Fatalf("Expected no blocking validation errors") } } func TestImport_Sorting(t *testing.T) { var imports Imports pk := publicKey(createAccountNKey(t), t) imports.Add(&Import{Subject: "x", Type: Service, Account: pk}) imports.Add(&Import{Subject: "z", Type: Service, Account: pk}) imports.Add(&Import{Subject: "y", Type: Service, Account: pk}) if imports[0].Subject != "x" { t.Fatal("added import not in expected order") } sort.Sort(imports) if imports[0].Subject != "x" && imports[1].Subject != "y" && imports[2].Subject != "z" { t.Fatal("imports not sorted") } } func TestImports_Validate(t *testing.T) { var imports Imports pk := publicKey(createAccountNKey(t), t) imports.Add(&Import{Subject: "x", LocalSubject: "foo", Type: Service, Account: pk}) imports.Add(&Import{Subject: "z.*", LocalSubject: "*", Type: Service, Account: pk}) imports.Add(&Import{Subject: "y.>", LocalSubject: ">", Type: Service, Account: pk}) vr := ValidationResults{} imports.Validate("", &vr) if len(vr.Issues) != 3 || !vr.IsBlocking(false) { t.Fatal("expected 3 blocking issues") } for _, v := range vr.Issues { if !strings.HasPrefix(v.Description, "overlapping subject namespace") { t.Fatalf("Expected every error to contain: overlapping subject namespace") } } } func TestImportAllowTrace(t *testing.T) { ak2 := createAccountNKey(t) akp2 := publicKey(ak2, t) // AllowTrace is only applicable to StreamImport i := &Import{Subject: "foo", Account: akp2, Type: Service, AllowTrace: true} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Fatalf("AllowTrace on service should have an validation issue") } issue := vr.Issues[0] if !strings.Contains(issue.Description, "AllowTrace only valid for stream import") { t.Fatalf("AllowTrace should be valid only for stream import, got %q", issue.Description) } i.Type = Stream vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Fatalf("validation should have been ok, got %+v", vr.Issues) } } jwt-2.7.3/v2/operator_claims.go000066400000000000000000000170201472706253100163770ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "net/url" "strconv" "strings" "github.com/nats-io/nkeys" ) // Operator specific claims type Operator struct { // Slice of other operator NKeys that can be used to sign on behalf of the main // operator identity. SigningKeys StringList `json:"signing_keys,omitempty"` // AccountServerURL is a partial URL like "https://host.domain.org:/jwt/v1" // tools will use the prefix and build queries by appending /accounts/ // 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. AccountServerURL string `json:"account_server_url,omitempty"` // A list of NATS urls (tls://host:port) where tools can connect to the server // using proper credentials. OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` // Identity of the system account SystemAccount string `json:"system_account,omitempty"` // Min Server version AssertServerVersion string `json:"assert_server_version,omitempty"` // Signing of subordinate objects will require signing keys StrictSigningKeyUsage bool `json:"strict_signing_key_usage,omitempty"` GenericFields } func ParseServerVersion(version string) (int, int, int, error) { if version == "" { return 0, 0, 0, nil } split := strings.Split(version, ".") if len(split) != 3 { return 0, 0, 0, fmt.Errorf("asserted server version must be of the form ..") } else if major, err := strconv.Atoi(split[0]); err != nil { return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[0]) } else if minor, err := strconv.Atoi(split[1]); err != nil { return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[1]) } else if update, err := strconv.Atoi(split[2]); err != nil { return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[2]) } else if major < 0 || minor < 0 || update < 0 { return 0, 0, 0, fmt.Errorf("asserted server version can'b contain negative values: %s", version) } else { return major, minor, update, nil } } // Validate checks the validity of the operators contents func (o *Operator) Validate(vr *ValidationResults) { if err := o.validateAccountServerURL(); err != nil { vr.AddError(err.Error()) } for _, v := range o.validateOperatorServiceURLs() { if v != nil { vr.AddError(v.Error()) } } for _, k := range o.SigningKeys { if !nkeys.IsValidPublicOperatorKey(k) { vr.AddError("%s is not an operator public key", k) } } if o.SystemAccount != "" { if !nkeys.IsValidPublicAccountKey(o.SystemAccount) { vr.AddError("%s is not an account public key", o.SystemAccount) } } if _, _, _, err := ParseServerVersion(o.AssertServerVersion); err != nil { vr.AddError("assert server version error: %s", err) } } func (o *Operator) validateAccountServerURL() error { if o.AccountServerURL != "" { // We don't care what kind of URL it is so long as it parses // and has a protocol. The account server may impose additional // constraints on the type of URLs that it is able to notify to u, err := url.Parse(o.AccountServerURL) if err != nil { return fmt.Errorf("error parsing account server url: %v", err) } if u.Scheme == "" { return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL) } } return nil } // ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url. func ValidateOperatorServiceURL(v string) error { // should be possible for the service url to not be expressed if v == "" { return nil } u, err := url.Parse(v) if err != nil { return fmt.Errorf("error parsing operator service url %q: %v", v, err) } if u.User != nil { return fmt.Errorf("operator service url %q - credentials are not supported", v) } if u.Path != "" { return fmt.Errorf("operator service url %q - paths are not supported", v) } lcs := strings.ToLower(u.Scheme) switch lcs { case "nats": return nil case "tls": return nil case "ws": return nil case "wss": return nil default: return fmt.Errorf("operator service url %q - protocol not supported (only 'nats', 'tls', 'ws', 'wss' only)", v) } } func (o *Operator) validateOperatorServiceURLs() []error { var errs []error for _, v := range o.OperatorServiceURLs { if v != "" { if err := ValidateOperatorServiceURL(v); err != nil { errs = append(errs, err) } } } return errs } // OperatorClaims define the data for an operator JWT type OperatorClaims struct { ClaimsData Operator `json:"nats,omitempty"` } // NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key func NewOperatorClaims(subject string) *OperatorClaims { if subject == "" { return nil } c := &OperatorClaims{} c.Subject = subject c.Issuer = subject return c } // DidSign checks the claims against the operator's public key and its signing keys func (oc *OperatorClaims) DidSign(op Claims) bool { if op == nil { return false } issuer := op.Claims().Issuer if issuer == oc.Subject { if !oc.StrictSigningKeyUsage { return true } return op.Claims().Subject == oc.Subject } return oc.SigningKeys.Contains(issuer) } // Encode the claims into a JWT string func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { return oc.EncodeWithSigner(pair, nil) } func (oc *OperatorClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicOperatorKey(oc.Subject) { return "", errors.New("expected subject to be an operator public key") } err := oc.validateAccountServerURL() if err != nil { return "", err } oc.Type = OperatorClaim return oc.ClaimsData.encode(pair, oc, fn) } func (oc *OperatorClaims) ClaimType() ClaimType { return oc.Type } // DecodeOperatorClaims tries to create an operator claims from a JWt string func DecodeOperatorClaims(token string) (*OperatorClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } oc, ok := claims.(*OperatorClaims) if !ok { return nil, errors.New("not operator claim") } return oc, nil } func (oc *OperatorClaims) String() string { return oc.ClaimsData.String(oc) } // Payload returns the operator specific data for an operator JWT func (oc *OperatorClaims) Payload() interface{} { return &oc.Operator } // Validate the contents of the claims func (oc *OperatorClaims) Validate(vr *ValidationResults) { oc.ClaimsData.Validate(vr) oc.Operator.Validate(vr) } // ExpectedPrefixes defines the nkey types that can sign operator claims, operator func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteOperator} } // Claims returns the generic claims data func (oc *OperatorClaims) Claims() *ClaimsData { return &oc.ClaimsData } func (oc *OperatorClaims) updateVersion() { oc.GenericFields.Version = libVersion } func (oc *OperatorClaims) GetTags() TagList { return oc.Operator.Tags } jwt-2.7.3/v2/operator_claims_test.go000066400000000000000000000303621472706253100174420ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "fmt" "testing" "time" "github.com/nats-io/nkeys" ) func TestNewOperatorClaims(t *testing.T) { ckp := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc.Expires = time.Now().Add(time.Hour).Unix() uJwt := encode(uc, ckp, t) uc2, err := DecodeOperatorClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestOperatorSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewOperatorClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode server with with %q subject", i.name) t.Fail() } } } func TestInvalidOperatorClaimIssuer(t *testing.T) { akp := createOperatorNKey(t) ac := NewOperatorClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Hour).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeOperatorClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestNewNilOperatorClaims(t *testing.T) { v := NewOperatorClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestOperatorType(t *testing.T) { c := NewOperatorClaims(publicKey(createOperatorNKey(t), t)) s := encode(c, createOperatorNKey(t), t) u, err := DecodeOperatorClaims(s) if err != nil { t.Fatalf("failed to decode operator claim: %v", err) } if OperatorClaim != u.Type { t.Fatalf("type is unexpected %q (wanted operator)", u.Type) } } func TestSigningKeyValidation(t *testing.T) { ckp := createOperatorNKey(t) ckp2 := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc.Expires = time.Now().Add(time.Hour).Unix() uc.SigningKeys.Add(publicKey(ckp2, t)) uJwt := encode(uc, ckp, t) uc2, err := DecodeOperatorClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(len(uc2.SigningKeys), 1, t) AssertEquals(uc2.SigningKeys[0] == publicKey(ckp2, t), true, t) vr := &ValidationResults{} uc.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid operator key should have no validation issues") } uc.SigningKeys.Add("") // add an invalid one vr = &ValidationResults{} uc.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("should not be able to add empty values") } } func TestSignedBy(t *testing.T) { ckp := createOperatorNKey(t) ckp2 := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc2 := NewOperatorClaims(publicKey(ckp2, t)) akp := createAccountNKey(t) ac := NewAccountClaims(publicKey(akp, t)) enc, err := ac.Encode(ckp) // sign with the operator key if err != nil { t.Fatal("failed to encode", err) } ac, err = DecodeAccountClaims(enc) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.DidSign(ac), true, t) AssertEquals(uc2.DidSign(ac), false, t) enc, err = ac.Encode(ckp2) // sign with the other operator key if err != nil { t.Fatal("failed to encode", err) } ac, err = DecodeAccountClaims(enc) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.DidSign(ac), false, t) // no signing key AssertEquals(uc2.DidSign(ac), true, t) // actual key uc.SigningKeys.Add(publicKey(ckp2, t)) AssertEquals(uc.DidSign(ac), true, t) // signing key uc.StrictSigningKeyUsage = true AssertEquals(uc.DidSign(uc), true, t) AssertEquals(uc.DidSign(ac), true, t) uc2.StrictSigningKeyUsage = true AssertEquals(uc2.DidSign(uc2), true, t) AssertEquals(uc2.DidSign(ac), false, t) } func testAccountWithAccountServerURL(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.AccountServerURL = u s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(oc.AccountServerURL, u, t) vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { errs := vr.Errors() return errs[0] } return nil } func Test_AccountServerURL(t *testing.T) { var asuTests = []struct { u string shouldFail bool }{ {"", false}, {"HTTP://foo.bar.com", false}, {"http://foo.bar.com/foo/bar", false}, {"http://user:pass@foo.bar.com/foo/bar", false}, {"https://foo.bar.com", false}, {"nats://foo.bar.com", false}, {"/hello", true}, } for i, tt := range asuTests { err := testAccountWithAccountServerURL(t, tt.u) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].u) } } } func Test_SystemAccount(t *testing.T) { operatorWithSystemAcc := func(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.SystemAccount = u s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(oc.SystemAccount, u, t) vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { return fmt.Errorf("%s", vr.Errors()[0]) } return nil } var asuTests = []struct { accKey string shouldFail bool }{ {"", false}, {"x", true}, {"ADZ547B24WHPLWOK7TMLNBSA7FQFXR6UM2NZ4HHNIB7RDFVZQFOZ4GQQ", false}, {"ADZ547B24WHPLWOK7TMLNBSA7FQFXR6UM2NZ4HHNIB7RDFVZQFOZ4777", true}, } for i, tt := range asuTests { err := operatorWithSystemAcc(t, tt.accKey) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].accKey) } } } func Test_AssertServerVersion(t *testing.T) { operatorWithAssertServerVer := func(t *testing.T, v string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.AssertServerVersion = v s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(oc.AssertServerVersion, v, t) vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { return fmt.Errorf("%s", vr.Errors()[0]) } return nil } var asuTests = []struct { assertVer string shouldFail bool }{ {"1.2.3", false}, {"10.2.3", false}, {"1.20.3", false}, {"1.2.30", false}, {"10.20.30", false}, {"0.0.0", false}, {"0.0", true}, {"0", true}, {"a", true}, {"a.b.c", true}, {"1..1", true}, {"1a.b.c", true}, {"-1.0.0", true}, {"1.-1.0", true}, {"1.0.-1", true}, } for i, tt := range asuTests { err := operatorWithAssertServerVer(t, tt.assertVer) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].assertVer) } } } func testOperatorWithOperatorServiceURL(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.OperatorServiceURLs.Add(u) s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } if u != "" { AssertEquals(oc.OperatorServiceURLs[0], u, t) } vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { errs := vr.Errors() return errs[0] } return nil } func Test_OperatorServiceURL(t *testing.T) { var asuTests = []struct { u string shouldFail bool }{ {"", false}, {"HTTP://foo.bar.com", true}, {"http://foo.bar.com/foo/bar", true}, {"nats://user:pass@foo.bar.com", true}, {"NATS://user:pass@foo.bar.com", true}, {"NATS://user@foo.bar.com", true}, {"nats://foo.bar.com/path", true}, {"tls://foo.bar.com/path", true}, {"/hello", true}, {"NATS://foo.bar.com", false}, {"TLS://foo.bar.com", false}, {"nats://foo.bar.com", false}, {"tls://foo.bar.com", false}, } for i, tt := range asuTests { err := testOperatorWithOperatorServiceURL(t, tt.u) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].u) } } // now test all of them in a single jwt kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) encoded := 0 shouldFail := 0 for _, v := range asuTests { oc.OperatorServiceURLs.Add(v.u) // list won't encode empty strings if v.u != "" { encoded++ } if v.shouldFail { shouldFail++ } } s, err := oc.Encode(kp) if err != nil { t.Fatal(err) } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(len(oc.OperatorServiceURLs), encoded, t) vr := ValidationResults{} oc.Validate(&vr) if vr.IsEmpty() { t.Fatal("should have had errors") } errs := vr.Errors() AssertEquals(len(errs), shouldFail, t) } func TestTags(t *testing.T) { okp := createOperatorNKey(t) opk := publicKey(okp, t) oc := NewOperatorClaims(opk) oc.Tags.Add("one") oc.Tags.Add("one") // duplicated tags should be ignored oc.Tags.Add("TWO") // should become lower case oc.Tags.Add("three") oJwt := encode(oc, okp, t) oc2, err := DecodeOperatorClaims(oJwt) if err != nil { t.Fatal(err) } if len(oc2.GenericFields.Tags) != 3 { t.Fatal("expected 3 tags") } for _, v := range oc.GenericFields.Tags { AssertFalse(v == "TWO", t) } AssertTrue(oc.GenericFields.Tags.Contains("one"), t) AssertTrue(oc.GenericFields.Tags.Contains("two"), t) AssertTrue(oc.GenericFields.Tags.Contains("three"), t) } func TestOperatorClaims_GetTags(t *testing.T) { okp := createOperatorNKey(t) opk := publicKey(okp, t) oc := NewOperatorClaims(opk) oc.Operator.Tags.Add("foo", "bar") tags := oc.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } token, err := oc.Encode(okp) if err != nil { t.Fatal("error encoding") } oc, err = DecodeOperatorClaims(token) if err != nil { t.Fatal("error decoding") } tags = oc.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } } func TestNewOperatorClaimSignerFn(t *testing.T) { kp := createOperatorNKey(t) ok := false oc := NewOperatorClaims(publicKey(kp, t)) token, err := oc.EncodeWithSigner(kp, func(pub string, data []byte) ([]byte, error) { ok = true return kp.Sign(data) }) if err != nil { t.Fatal(err) } if !ok { t.Fatal("expected ok to be true") } oc, err = DecodeOperatorClaims(token) if err != nil { t.Fatal("failed to decode", err) } vr := CreateValidationResults() oc.Validate(vr) if !vr.IsEmpty() { t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) } } jwt-2.7.3/v2/revocation_list.go000066400000000000000000000050111472706253100164150ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "time" ) const All = "*" // RevocationList is used to store a mapping of public keys to unix timestamps type RevocationList map[string]int64 type RevocationEntry struct { PublicKey string TimeStamp int64 } // Revoke enters a revocation by publickey and timestamp into this export // If there is already a revocation for this public key that is newer, it is kept. func (r RevocationList) Revoke(pubKey string, timestamp time.Time) { newTS := timestamp.Unix() // cannot move a revocation into the future - only into the past if ts, ok := r[pubKey]; ok && ts > newTS { return } r[pubKey] = newTS } // MaybeCompact will compact the revocation list if jwt.All is found. Any // revocation that is covered by a jwt.All revocation will be deleted, thus // reducing the size of the JWT. Returns a slice of entries that were removed // during the process. func (r RevocationList) MaybeCompact() []RevocationEntry { var deleted []RevocationEntry ats, ok := r[All] if ok { for k, ts := range r { if k != All && ats >= ts { deleted = append(deleted, RevocationEntry{ PublicKey: k, TimeStamp: ts, }) delete(r, k) } } } return deleted } // ClearRevocation removes any revocation for the public key func (r RevocationList) ClearRevocation(pubKey string) { delete(r, pubKey) } // IsRevoked checks if the public key is in the revoked list with a timestamp later than // the one passed in. Generally this method is called with an issue time but other time's can // be used for testing. func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool { if r.allRevoked(timestamp) { return true } ts, ok := r[pubKey] return ok && ts >= timestamp.Unix() } // allRevoked returns true if All is set and the timestamp is later or same as the // one passed. This is called by IsRevoked. func (r RevocationList) allRevoked(timestamp time.Time) bool { ts, ok := r[All] return ok && ts >= timestamp.Unix() } jwt-2.7.3/v2/revocation_list_test.go000066400000000000000000000035521472706253100174640ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "sort" "testing" "time" ) func TestRevocationCompact(t *testing.T) { a := NewAccountClaims(publicKey(createAccountNKey(t), t)) now := time.Now() var keys []string keys = append(keys, publicKey(createUserNKey(t), t)) keys = append(keys, publicKey(createUserNKey(t), t)) keys = append(keys, publicKey(createUserNKey(t), t)) sort.Strings(keys) a.Revocations = make(RevocationList) a.Revocations.Revoke(keys[0], now.Add(-time.Hour)) a.Revocations.Revoke(keys[1], now.Add(-time.Minute)) a.Revocations.Revoke(keys[2], now.Add(-time.Second)) // no change expected - there's no deleted := a.Revocations.MaybeCompact() if len(a.Revocations) != 3 || deleted != nil { t.Error("expected 3 revocations") } // should delete the first key a.Revocations.Revoke(All, now.Add(-time.Minute*30)) deleted = a.Revocations.MaybeCompact() if len(a.Revocations) != 3 && len(deleted) != 1 && deleted[0].PublicKey != keys[0] { t.Error("expected 3 revocations") } // should delete the 2 remaining keys, only All remains a.Revocations.Revoke(All, now.Add(-time.Second)) deleted = a.Revocations.MaybeCompact() if len(a.Revocations) != 1 && len(deleted) != 2 && deleted[0].PublicKey != keys[1] && deleted[1].PublicKey != keys[2] { t.Error("didn't revoke expected entries") } } jwt-2.7.3/v2/signingkeys.go000066400000000000000000000107051472706253100155510ustar00rootroot00000000000000/* * Copyright 2020-2024 The NATS Authors * 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. */ package jwt import ( "encoding/json" "errors" "fmt" "sort" "github.com/nats-io/nkeys" ) type Scope interface { SigningKey() string ValidateScopedSigner(claim Claims) error Validate(vr *ValidationResults) } type ScopeType int const ( UserScopeType ScopeType = iota + 1 ) func (t ScopeType) String() string { switch t { case UserScopeType: return "user_scope" } return "unknown" } func (t *ScopeType) MarshalJSON() ([]byte, error) { switch *t { case UserScopeType: return []byte("\"user_scope\""), nil } return nil, fmt.Errorf("unknown scope type %q", t) } func (t *ScopeType) UnmarshalJSON(b []byte) error { var s string err := json.Unmarshal(b, &s) if err != nil { return err } switch s { case "user_scope": *t = UserScopeType return nil } return fmt.Errorf("unknown scope type %q", t) } type UserScope struct { Kind ScopeType `json:"kind"` Key string `json:"key"` Role string `json:"role"` Template UserPermissionLimits `json:"template"` Description string `json:"description"` } func NewUserScope() *UserScope { var s UserScope s.Kind = UserScopeType s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit} return &s } func (us UserScope) SigningKey() string { return us.Key } func (us UserScope) Validate(vr *ValidationResults) { if !nkeys.IsValidPublicAccountKey(us.Key) { vr.AddError("%s is not an account public key", us.Key) } } func (us UserScope) ValidateScopedSigner(c Claims) error { uc, ok := c.(*UserClaims) if !ok { return fmt.Errorf("not an user claim - scoped signing key requires user claim") } if uc.Claims().Issuer != us.Key { return errors.New("issuer not the scoped signer") } if !uc.HasEmptyPermissions() { return errors.New("scoped users require no permissions or limits set") } return nil } // SigningKeys is a map keyed by a public account key type SigningKeys map[string]Scope func (sk SigningKeys) Validate(vr *ValidationResults) { for k, v := range sk { // regular signing keys won't have a scope if v != nil { v.Validate(vr) } else { if !nkeys.IsValidPublicAccountKey(k) { vr.AddError("%q is not a valid account signing key", k) } } } } // MarshalJSON serializes the scoped signing keys as an array func (sk *SigningKeys) MarshalJSON() ([]byte, error) { if sk == nil { return nil, nil } keys := sk.Keys() sort.Strings(keys) var a []interface{} for _, k := range keys { if (*sk)[k] != nil { a = append(a, (*sk)[k]) } else { a = append(a, k) } } return json.Marshal(a) } func (sk *SigningKeys) UnmarshalJSON(data []byte) error { if *sk == nil { *sk = make(SigningKeys) } // read an array - we can have a string or an map var a []interface{} if err := json.Unmarshal(data, &a); err != nil { return err } for _, i := range a { switch v := i.(type) { case string: (*sk)[v] = nil case map[string]interface{}: d, err := json.Marshal(v) if err != nil { return err } switch v["kind"] { case UserScopeType.String(): us := NewUserScope() if err := json.Unmarshal(d, &us); err != nil { return err } (*sk)[us.Key] = us default: return fmt.Errorf("unknown signing key scope %q", v["type"]) } } } return nil } func (sk SigningKeys) Keys() []string { var keys []string for k := range sk { keys = append(keys, k) } return keys } // GetScope returns nil if the key is not associated func (sk SigningKeys) GetScope(k string) (Scope, bool) { v, ok := sk[k] if !ok { return nil, false } return v, true } func (sk SigningKeys) Contains(k string) bool { _, ok := sk[k] return ok } func (sk SigningKeys) Add(keys ...string) { for _, k := range keys { sk[k] = nil } } func (sk SigningKeys) AddScopedSigner(s Scope) { sk[s.SigningKey()] = s } func (sk SigningKeys) Remove(keys ...string) { for _, k := range keys { delete(sk, k) } } jwt-2.7.3/v2/signingkeys_test.go000066400000000000000000000177451472706253100166230ustar00rootroot00000000000000/* * Copyright 2020-2024 The NATS Authors * 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. */ package jwt import ( "encoding/json" "strings" "testing" "github.com/nats-io/nkeys" ) func makeRole(t *testing.T, role string, pub []string, sub []string, bearer bool) (*UserScope, nkeys.KeyPair) { akp := createAccountNKey(t) pk := publicKey(akp, t) r := NewUserScope() r.Key = pk r.Template.BearerToken = bearer r.Template.Sub.Allow.Add(sub...) r.Template.Pub.Allow.Add(pub...) r.Template.BearerToken = bearer r.Role = role return r, akp } func makeUser(t *testing.T, ac *AccountClaims, signer nkeys.KeyPair, pub []string, sub []string) *UserClaims { ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) if pub == nil && sub == nil { uc.UserPermissionLimits = UserPermissionLimits{} } spk := publicKey(signer, t) if ac.Subject != spk { uc.IssuerAccount = ac.Subject } uc.Pub.Allow.Add(pub...) uc.Sub.Allow.Add(sub...) ut, err := uc.Encode(signer) if err != nil { t.Fatal(err) } uc, err = DecodeUserClaims(ut) if err != nil { t.Fatal(err) } for _, p := range pub { if !uc.Pub.Allow.Contains(p) { t.Fatalf("expected user to have pub %q", p) } } for _, p := range pub { if !uc.Pub.Allow.Contains(p) { t.Fatalf("expected user to have pub %q", p) } } for _, s := range sub { if !uc.Sub.Allow.Contains(s) { t.Fatalf("expected user to have sub %q", s) } } return uc } func makeAccount(t *testing.T, sks []string, roles []Scope) (*AccountClaims, nkeys.KeyPair) { akp := createAccountNKey(t) pk := publicKey(akp, t) ac := NewAccountClaims(pk) ac.SigningKeys.Add(sks...) for _, r := range roles { ac.SigningKeys.AddScopedSigner(r) } token, err := ac.Encode(createOperatorNKey(t)) if err != nil { t.Fatal(err) } ac, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } for _, k := range sks { if !ac.SigningKeys.Contains(k) { t.Fatalf("expected to find signer: %s", k) } } for _, r := range roles { rr, _ := ac.SigningKeys.GetScope(r.SigningKey()) if rr == nil { t.Fatalf("expected scope for signer %s", r.SigningKey()) } } return ac, akp } func TestScopesAreAccounts(t *testing.T) { // make a bad role that has user sk bad, _ := makeRole(t, "bad", []string{">"}, nil, false) bad.Key = publicKey(createUserNKey(t), t) ac, _ := makeAccount(t, nil, []Scope{bad}) var vr ValidationResults ac.Validate(&vr) if vr.IsEmpty() { t.Fatal("should have had validation errors") } if len(vr.Errors()) != 1 { t.Fatal("expected one error") } if !strings.Contains(vr.Errors()[0].Error(), bad.Key) { t.Fatal("expected error to be about the user key") } } func TestScopesCheckIssuer(t *testing.T) { // make a bad role that has user sk r, _ := makeRole(t, "bad", []string{">"}, nil, false) ac, akp := makeAccount(t, nil, []Scope{r}) uc := makeUser(t, ac, akp, nil, nil) err := r.ValidateScopedSigner(uc) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "issuer not the scoped signer") { t.Fatalf("expected scoped signer error - but got: %v", err) } } func TestScopesCheckClaimType(t *testing.T) { // make a bad role that has user sk r, _ := makeRole(t, "bad", []string{">"}, nil, false) ac, _ := makeAccount(t, nil, []Scope{r}) err := r.ValidateScopedSigner(ac) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "not an user claim") { t.Fatalf("expected claim type error - but got: %v", err) } } func TestScopedSigningKeysBasics(t *testing.T) { adm, _ := makeRole(t, "admin", []string{">"}, []string{">"}, false) dash, dashKP := makeRole(t, "dashboard", []string{"dashboard.>"}, []string{"dashboard.>"}, true) signer := createAccountNKey(t) signerPK := publicKey(signer, t) ac, apk := makeAccount(t, []string{signerPK}, []Scope{adm, dash}) // test a user issued by the account uc := makeUser(t, ac, apk, nil, nil) if !ac.DidSign(uc) { t.Fatalf("should have been a valid user") } _, ok := ac.SigningKeys.GetScope(uc.Issuer) if ok { t.Fatal("this was issued by the account") } // test a user issued by a the signer uc = makeUser(t, ac, signer, nil, nil) if !ac.DidSign(uc) { t.Fatalf("should have been a valid user") } scope, ok := ac.SigningKeys.GetScope(uc.Issuer) if !ok { t.Fatal("signer should have been found") } if scope != nil { t.Fatal("unexpected scope") } // test a user with a scope uc = makeUser(t, ac, dashKP, nil, nil) if !ac.DidSign(uc) { t.Fatalf("should have been a valid user") } scope, ok = ac.SigningKeys.GetScope(uc.Issuer) if !ok { t.Fatal("signer should have been found") } if scope == nil { t.Fatal("expected scope") } if scope.SigningKey() != publicKey(dashKP, t) { t.Fatal("expected scope to be dashboard key") } if err := scope.ValidateScopedSigner(uc); err != nil { t.Fatalf("expected scope to be correct: %v", err) } // test user with a scope that has wrong permissions uc = makeUser(t, ac, dashKP, []string{">"}, nil) if !ac.DidSign(uc) { t.Fatalf("should have been a valid user") } scope, ok = ac.SigningKeys.GetScope(uc.Issuer) if !ok { t.Fatal("signer should have been found") } if scope == nil { t.Fatal("expected scope") } if scope.SigningKey() != publicKey(dashKP, t) { t.Fatal("expected scope to be dashboard key") } if err := scope.ValidateScopedSigner(uc); err == nil { t.Fatalf("expected scope to reject user") } } func TestGetKeys(t *testing.T) { ac, apk := makeAccount(t, nil, nil) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) token, err := ac.Encode(apk) if err != nil { t.Fatal(err) } aac, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } keys := aac.SigningKeys.Keys() if len(keys) != 3 { t.Fatal("expected 3 signing keys") } for _, k := range keys { if !ac.SigningKeys.Contains(k) { t.Fatal("expected to find key") } } } func TestScopeJSON(t *testing.T) { ac, apk := makeAccount(t, nil, nil) pk := publicKey(createAccountNKey(t), t) us := NewUserScope() us.Key = pk us.Role = "Admin" us.Description = "Admin Key" us.Template = UserPermissionLimits{ Permissions: Permissions{ Pub: Permission{Allow: []string{"foo"}}, }, } ac.SigningKeys.AddScopedSigner(us) token, err := ac.Encode(apk) if err != nil { t.Fatal(err) } ac, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } s, ok := ac.SigningKeys.GetScope(pk) if !ok { t.Fatal("expected to find a scope admin") } us, ok = s.(*UserScope) if !ok { t.Fatal("expected to find an user scope") } if us.Key != pk { t.Fatal("expected public key to match") } if !us.Template.Permissions.Pub.Allow.Contains("foo") { t.Fatal("expected permissions to contain foo") } if us.Description != "Admin Key" { t.Fatal("expected description to match") } if us.Role != "Admin" { t.Fatal("expected role to match") } } func TestJson(t *testing.T) { ac, apk := makeAccount(t, nil, nil) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) ac.SigningKeys.Add(publicKey(createAccountNKey(t), t)) token, err := ac.Encode(apk) if err != nil { t.Fatal(err) } aac, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } j, err := json.Marshal(aac) if err != nil { t.Fatal(err) } var myAcc AccountClaims err = json.Unmarshal(j, &myAcc) if err != nil { t.Fatal(err) } if len(myAcc.SigningKeys) != 3 { t.Fatalf("Expected 3 signing keys got: %d", len(myAcc.SigningKeys)) } } jwt-2.7.3/v2/test/000077500000000000000000000000001472706253100136445ustar00rootroot00000000000000jwt-2.7.3/v2/test/decoder_migration_test.go000066400000000000000000000246451472706253100207230ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "strings" "testing" "time" "github.com/nats-io/nkeys" . "github.com/nats-io/jwt/v2" v1jwt "github.com/nats-io/jwt/v2/v1compat" ) func createExport(sub string) *v1jwt.Export { var e v1jwt.Export e.Type = v1jwt.Service e.Subject = v1jwt.Subject(sub) e.Name = "foo" e.TokenReq = true e.ResponseType = v1jwt.ResponseTypeSingleton return &e } func createImport(t *testing.T, e *v1jwt.Export, target string, signer nkeys.KeyPair) *v1jwt.Import { var i v1jwt.Import i.Account = target i.Subject = e.Subject i.Type = e.Type i.Name = e.Name if e.TokenReq { i.Token = createActivation(t, e, target, signer) i.To = v1jwt.Subject(e.Name) } return &i } func createActivation(t *testing.T, e *v1jwt.Export, target string, signer nkeys.KeyPair) string { ac := v1jwt.NewActivationClaims(target) ac.Name = e.Name ac.ImportType = e.Type s := strings.Replace(string(e.Subject), "*", target, -1) ac.ImportSubject = v1jwt.Subject(s) tok, err := ac.Encode(signer) AssertNoError(err, t) return tok } func TestMigrateOperator(t *testing.T) { okp, err := nkeys.CreateOperator() AssertNoError(err, t) opk, err := okp.PublicKey() AssertNoError(err, t) sapk, err := okp.PublicKey() AssertNoError(err, t) oc := v1jwt.NewOperatorClaims(opk) oc.Name = "O" oc.Audience = "Audience" now := time.Now() oc.NotBefore = now.Unix() e := now.Add(time.Hour) oc.ClaimsData.Expires = e.Unix() oc.Tags.Add("a") oc.OperatorServiceURLs.Add("nats://localhost:4222") oc.AccountServerURL = "http://localhost:9090/jwt/v1" oc.SystemAccount = sapk sk, err := nkeys.CreateOperator() AssertNoError(err, t) psk, err := sk.PublicKey() AssertNoError(err, t) oc.Operator.SigningKeys.Add(psk) oc.Identities = append(oc.Identities, v1jwt.Identity{ ID: "O", Proof: "http://www.o.com/o", }) token, err := oc.Encode(okp) AssertNoError(err, t) c, err := Decode(token) AssertNoError(err, t) oc2, ok := c.(*OperatorClaims) AssertTrue(ok, t) equalOperators(t, oc, oc2) AssertEquals(oc2.Version, 1, t) } func TestMigrateAccount(t *testing.T) { okp, err := nkeys.CreateOperator() AssertNoError(err, t) akp, err := nkeys.CreateAccount() AssertNoError(err, t) apk, err := akp.PublicKey() AssertNoError(err, t) ac := v1jwt.NewAccountClaims(apk) ac.Name = "A" ac.Audience = "Audience" now := time.Now() ac.NotBefore = now.Unix() e := now.Add(time.Hour) ac.ClaimsData.Expires = e.Unix() ac.Tags.Add("a") // create an import ea, err := nkeys.CreateAccount() AssertNoError(err, t) hex := createExport("help") ac.Imports.Add(createImport(t, hex, apk, ea)) // add an export ac.Exports = append(ac.Exports, createExport("q")) // add an identity ac.Identities = append(ac.Identities, v1jwt.Identity{ ID: "A", Proof: "http://www.a.com/a", }) // set the limits ac.Limits.Subs = 1 ac.Limits.Conn = 2 ac.Limits.LeafNodeConn = 4 ac.Limits.Imports = 8 ac.Limits.Exports = 16 ac.Limits.Data = 32 ac.Limits.Payload = 64 ac.Limits.WildcardExports = true // add a signing key sk, err := nkeys.CreateAccount() AssertNoError(err, t) psk, err := sk.PublicKey() AssertNoError(err, t) ac.Account.SigningKeys.Add(psk) // add a revocation ukp, err := nkeys.CreateUser() AssertNoError(err, t) upk, err := ukp.PublicKey() AssertNoError(err, t) ac.Revocations = make(map[string]int64) ac.Revocations.Revoke(upk, time.Now()) token, err := ac.Encode(okp) AssertNoError(err, t) c, err := Decode(token) AssertNoError(err, t) ac2, ok := c.(*AccountClaims) AssertTrue(ok, t) equalAccounts(t, ac, ac2) AssertEquals(ac2.Version, 1, t) } func TestMigrateUser(t *testing.T) { ukp, err := nkeys.CreateUser() AssertNoError(err, t) upk, err := ukp.PublicKey() AssertNoError(err, t) uc := v1jwt.NewUserClaims(upk) uc.Name = "U" uc.Audience = "Audience" uc.Src = " 127.0.0.1/1 , 127.0.0.1/2 " now := time.Now() uc.NotBefore = now.Unix() e := now.Add(time.Hour) uc.ClaimsData.Expires = e.Unix() uc.Tags.Add("a") uc.Permissions.Sub.Allow.Add("q") uc.Permissions.Sub.Deny.Add("d") uc.Permissions.Pub.Allow.Add("help") uc.Permissions.Pub.Deny.Add("pleh") uc.Permissions.Resp = &v1jwt.ResponsePermission{} uc.Permissions.Resp.MaxMsgs = 100 uc.Permissions.Resp.Expires = time.Second uc.BearerToken = true akp, err := nkeys.CreateAccount() AssertNoError(err, t) tok, err := uc.Encode(akp) AssertNoError(err, t) c, err := Decode(tok) AssertNoError(err, t) uc2, ok := c.(*UserClaims) AssertTrue(ok, t) AssertTrue(uc2.Limits.Payload == NoLimit, t) AssertTrue(uc2.Limits.Subs == NoLimit, t) AssertTrue(uc2.Limits.Data == NoLimit, t) AssertTrue(len(uc2.Src) == 2, t) AssertTrue(uc2.Src.Contains("127.0.0.1/1"), t) AssertTrue(uc2.Src.Contains("127.0.0.1/2"), t) AssertEquals(uc2.Version, 1, t) equalUsers(t, uc, uc2) } func TestMigrateUserWithDeprecatedLimits(t *testing.T) { ukp, err := nkeys.CreateUser() AssertNoError(err, t) upk, err := ukp.PublicKey() AssertNoError(err, t) akp, err := nkeys.CreateAccount() AssertNoError(err, t) uc := v1jwt.NewUserClaims(upk) uc.Name = "U" uc.Audience = "Audience" uc.Max = 1 tok, err := uc.Encode(akp) AssertNoError(err, t) _, err = Decode(tok) AssertNoError(err, t) } func TestMigrateUserToGeneric(t *testing.T) { ukp, err := nkeys.CreateUser() AssertNoError(err, t) upk, err := ukp.PublicKey() AssertNoError(err, t) akp, err := nkeys.CreateAccount() AssertNoError(err, t) uc := v1jwt.NewUserClaims(upk) uc.Name = "U" uc.Audience = "Audience" uc.Max = 1 uc.Tags = []string{"foo", "bar"} tok, err := uc.Encode(akp) AssertNoError(err, t) uc2, err := DecodeGeneric(tok) AssertNoError(err, t) AssertTrue(string(uc2.ClaimType()) == string(uc.Type), t) AssertTrue(uc2.Data["tags"].(TagList)[0] == uc.Tags[0], t) } func TestMigrateActivationWithDeprecatedLimits(t *testing.T) { akp, err := nkeys.CreateAccount() AssertNoError(err, t) apk, err := akp.PublicKey() AssertNoError(err, t) acOrig := v1jwt.NewActivationClaims(apk) ac := acOrig ac.Max = 1 tok, err := ac.Encode(akp) AssertNoError(err, t) _, err = Decode(tok) AssertNoError(err, t) ac = acOrig ac.Src = "foo" tok, err = ac.Encode(akp) AssertNoError(err, t) _, err = Decode(tok) AssertNoError(err, t) ac = acOrig ac.Limits.Payload = 5 tok, err = ac.Encode(akp) AssertNoError(err, t) _, err = Decode(tok) AssertNoError(err, t) ac = acOrig ac.Times = append(ac.Times, v1jwt.TimeRange{ Start: "15:43:22", End: "27:11:11", }) tok, err = ac.Encode(akp) AssertNoError(err, t) _, err = Decode(tok) AssertNoError(err, t) } func equalClaims(t *testing.T, o *v1jwt.ClaimsData, n *ClaimsData, gf *GenericFields) { AssertEquals(o.Subject, n.Subject, t) AssertEquals(o.Issuer, n.Issuer, t) AssertEquals(o.Name, n.Name, t) AssertEquals(o.Audience, n.Audience, t) AssertEquals(o.NotBefore, n.NotBefore, t) AssertEquals(o.Expires, n.Expires, t) AssertEquals(string(o.Type), string(gf.Type), t) AssertTrue(len(o.Tags) == len(gf.Tags), t) for _, v := range gf.Tags { AssertTrue(o.Tags.Contains(v), t) } } func equalOperators(t *testing.T, o *v1jwt.OperatorClaims, n *OperatorClaims) { equalClaims(t, &o.ClaimsData, &n.ClaimsData, &n.GenericFields) for _, v := range o.OperatorServiceURLs { AssertTrue(n.OperatorServiceURLs.Contains(v), t) } for _, v := range o.SigningKeys { AssertTrue(n.Operator.SigningKeys.Contains(v), t) } AssertEquals(o.SystemAccount, o.Operator.SystemAccount, t) } func equalAccounts(t *testing.T, o *v1jwt.AccountClaims, n *AccountClaims) { equalClaims(t, &o.ClaimsData, &n.ClaimsData, &n.GenericFields) equalImports(t, o.Imports[0], n.Imports[0]) equalExports(t, o.Exports[0], n.Exports[0]) equalLimits(t, &o.Account.Limits, &n.Account.Limits) for _, v := range o.SigningKeys { AssertTrue(n.Account.SigningKeys.Contains(v), t) } } func equalUsers(t *testing.T, o *v1jwt.UserClaims, n *UserClaims) { equalClaims(t, &o.ClaimsData, &n.ClaimsData, &n.GenericFields) for _, v := range o.Sub.Allow { AssertTrue(n.Sub.Allow.Contains(v), t) } for _, v := range o.Pub.Allow { AssertTrue(n.Pub.Allow.Contains(v), t) } for _, v := range o.Sub.Deny { AssertTrue(n.Sub.Deny.Contains(v), t) } for _, v := range o.Pub.Deny { AssertTrue(n.Pub.Deny.Contains(v), t) } if o.User.Resp == nil { AssertNil(n.User.Resp, t) } else { AssertEquals(o.User.Resp.Expires, n.User.Resp.Expires, t) AssertEquals(o.User.Resp.MaxMsgs, n.User.Resp.MaxMsgs, t) } if o.IssuerAccount != "" { AssertEquals(o.IssuerAccount, n.User.IssuerAccount, t) } AssertEquals(o.User.BearerToken, n.User.BearerToken, t) } func equalExports(t *testing.T, o *v1jwt.Export, n *Export) { AssertEquals(o.Name, n.Name, t) AssertEquals(string(o.Subject), string(n.Subject), t) AssertEquals(int(o.Type), int(n.Type), t) AssertEquals(o.TokenReq, n.TokenReq, t) AssertEquals(string(o.ResponseType), string(n.ResponseType), t) } func equalImports(t *testing.T, o *v1jwt.Import, n *Import) { AssertEquals(o.Name, n.Name, t) AssertEquals(string(o.Subject), string(n.Subject), t) //lint:ignore SA1019 testing AssertEquals(string(o.To), string(n.To), t) AssertEquals(int(o.Type), int(n.Type), t) if o.Token != "" { ot, err := v1jwt.DecodeActivationClaims(o.Token) AssertNoError(err, t) nt, err := DecodeActivationClaims(n.Token) AssertNoError(err, t) equalActivation(t, ot, nt) } } func equalActivation(t *testing.T, o *v1jwt.ActivationClaims, n *ActivationClaims) { equalClaims(t, &o.ClaimsData, &n.ClaimsData, &n.Activation.GenericFields) AssertEquals(string(o.ImportSubject), string(n.ImportSubject), t) AssertEquals(int(o.ImportType), int(n.ImportType), t) } func equalLimits(t *testing.T, o *v1jwt.OperatorLimits, n *OperatorLimits) { AssertEquals(o.Subs, n.Subs, t) AssertEquals(o.Conn, n.Conn, t) AssertEquals(o.LeafNodeConn, n.LeafNodeConn, t) AssertEquals(o.Imports, n.Imports, t) AssertEquals(o.Exports, n.Exports, t) AssertEquals(o.Data, n.Data, t) AssertEquals(o.Payload, n.Payload, t) AssertEquals(o.WildcardExports, n.WildcardExports, t) } jwt-2.7.3/v2/test/genericclaims_test.go000066400000000000000000000075431472706253100200500ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "testing" "time" . "github.com/nats-io/jwt/v2" jwtv1 "github.com/nats-io/jwt/v2/v1compat" ) func TestNewGenericClaims(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) gc := NewGenericClaims(apk) gc.Expires = time.Now().Add(time.Hour).UTC().Unix() gc.Name = "alberto" gc.Audience = "everyone" gc.NotBefore = time.Now().UTC().Unix() gc.Data["test"] = true gcJwt := encode(gc, akp, t) uc2, err := DecodeGeneric(gcJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(gc.String(), uc2.String(), t) AssertEquals(gc.Name, uc2.Name, t) AssertEquals(gc.Audience, uc2.Audience, t) AssertEquals(gc.Expires, uc2.Expires, t) AssertEquals(gc.NotBefore, uc2.NotBefore, t) AssertEquals(gc.Subject, uc2.Subject, t) AssertEquals(gc.Data["test"], true, t) AssertEquals(gc.Claims() != nil, true, t) AssertEquals(gc.Payload() != nil, true, t) } func TestNewGenericOperatorClaims(t *testing.T) { okp := createOperatorNKey(t) opk := publicKey(okp, t) op := NewOperatorClaims(opk) oJwt := encode(op, okp, t) oc2, err := DecodeGeneric(oJwt) if err != nil { t.Fatal("failed to decode", err) } if OperatorClaim != oc2.ClaimType() { t.Fatalf("Bad Claim type") } } func TestGenericClaimsCanHaveCustomType(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) gc := NewGenericClaims(apk) gc.Expires = time.Now().Add(time.Hour).UTC().Unix() gc.Name = "alberto" gc.Data["hello"] = "world" gc.Data["count"] = 5 gc.Data["type"] = "my_type" gcJwt := encode(gc, akp, t) gc2, err := DecodeGeneric(gcJwt) if err != nil { t.Fatal("failed to decode", err) } if gc2.ClaimType() != GenericClaim { t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) } if gc2.Data["type"] != "my_type" { t.Fatalf("expected internal type to be 'my_type': %v", gc2.Data["type"]) } } func TestGenericClaimsCanHaveCustomTypeFromV1(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) gc := jwtv1.NewGenericClaims(apk) gc.Expires = time.Now().Add(time.Hour).UTC().Unix() gc.Name = "alberto" gc.Data["hello"] = "world" gc.Data["count"] = 5 gc.Type = "my_type" token, err := gc.Encode(akp) if err != nil { t.Fatalf("failed to encode v1 JWT: %v", err) } gc2, err := DecodeGeneric(token) if err != nil { t.Fatal("failed to decode", err) } if gc2.ClaimType() != GenericClaim { t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) } if gc2.Data["type"] != "my_type" { t.Fatalf("expected internal type to be 'my_type': %v", gc2.Data["type"]) } } func TestGenericClaimsSignerFn(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) gc := NewGenericClaims(apk) gc.Expires = time.Now().Add(time.Hour).UTC().Unix() gc.Name = "alberto" gc.Data["hello"] = "world" gc.Data["count"] = 5 gc.Data["type"] = "my_type" ok := false gcJwt, err := gc.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { ok = true return akp.Sign(data) }) if err != nil { t.Fatal("failed to encode") } if !ok { t.Fatal("didn't encode with function") } gc2, err := DecodeGeneric(gcJwt) if err != nil { t.Fatal("failed to decode", err) } if gc2.ClaimType() != GenericClaim { t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) } } jwt-2.7.3/v2/test/util_test.go000066400000000000000000000042711472706253100162130ustar00rootroot00000000000000/* * Copyright 2018-2021 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "runtime" "strings" "testing" "github.com/nats-io/nkeys" . "github.com/nats-io/jwt/v2" ) func Trace(message string) string { lines := make([]string, 0, 32) err := errors.New(message) msg := err.Error() lines = append(lines, msg) for i := 2; true; i++ { _, file, line, ok := runtime.Caller(i) if !ok { break } msg := fmt.Sprintf("%s:%d", file, line) lines = append(lines, msg) } return strings.Join(lines, "\n") } func AssertEquals(expected, v interface{}, t *testing.T) { if expected != v { t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) } } func AssertNil(v interface{}, t *testing.T) { if v != nil { t.FailNow() } } func AssertNoError(err error, t *testing.T) { if err != nil { t.Fatal(err) } } func AssertTrue(condition bool, t *testing.T) { if !condition { t.FailNow() } } func AssertFalse(condition bool, t *testing.T) { if condition { t.FailNow() } } func createAccountNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("error creating account kp", err) } return kp } func createOperatorNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateOperator() if err != nil { t.Fatal("error creating operator kp", err) } return kp } func publicKey(kp nkeys.KeyPair, t *testing.T) string { pk, err := kp.PublicKey() if err != nil { t.Fatal("error reading public key", err) } return pk } func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { s, err := c.Encode(kp) if err != nil { t.Fatal("error encoding claim", err) } return s } jwt-2.7.3/v2/types.go000066400000000000000000000264071472706253100143710ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "net" "net/url" "reflect" "strconv" "strings" "time" ) const MaxInfoLength = 8 * 1024 type Info struct { Description string `json:"description,omitempty"` InfoURL string `json:"info_url,omitempty"` } func (s Info) Validate(vr *ValidationResults) { if len(s.Description) > MaxInfoLength { vr.AddError("Description is too long") } if s.InfoURL != "" { if len(s.InfoURL) > MaxInfoLength { vr.AddError("Info URL is too long") } u, err := url.Parse(s.InfoURL) if err == nil && (u.Hostname() == "" || u.Scheme == "") { err = fmt.Errorf("no hostname or scheme") } if err != nil { vr.AddError("error parsing info url: %v", err) } } } // ExportType defines the type of import/export. type ExportType int const ( // Unknown is used if we don't know the type Unknown ExportType = iota // Stream defines the type field value for a stream "stream" Stream // Service defines the type field value for a service "service" Service ) func (t ExportType) String() string { switch t { case Stream: return "stream" case Service: return "service" } return "unknown" } // MarshalJSON marshals the enum as a quoted json string func (t *ExportType) MarshalJSON() ([]byte, error) { switch *t { case Stream: return []byte("\"stream\""), nil case Service: return []byte("\"service\""), nil } return nil, fmt.Errorf("unknown export type") } // UnmarshalJSON unmashals a quoted json string to the enum value func (t *ExportType) UnmarshalJSON(b []byte) error { var j string err := json.Unmarshal(b, &j) if err != nil { return err } switch j { case "stream": *t = Stream return nil case "service": *t = Service return nil } return fmt.Errorf("unknown export type %q", j) } type RenamingSubject Subject func (s RenamingSubject) Validate(from Subject, vr *ValidationResults) { v := Subject(s) v.Validate(vr) if from == "" { vr.AddError("subject cannot be empty") } if strings.Contains(string(s), " ") { vr.AddError("subject %q cannot have spaces", v) } matchesSuffix := func(s Subject) bool { return s == ">" || strings.HasSuffix(string(s), ".>") } if matchesSuffix(v) != matchesSuffix(from) { vr.AddError("both, renaming subject and subject, need to end or not end in >") } fromCnt := from.countTokenWildcards() refCnt := 0 for _, tk := range strings.Split(string(v), ".") { if tk == "*" { refCnt++ } if len(tk) < 2 { continue } if tk[0] == '$' { if idx, err := strconv.Atoi(tk[1:]); err == nil { if idx > fromCnt { vr.AddError("Reference $%d in %q reference * in %q that do not exist", idx, s, from) } else { refCnt++ } } } } if refCnt != fromCnt { vr.AddError("subject does not contain enough * or reference wildcards $[0-9]") } } // Replaces reference tokens with * func (s RenamingSubject) ToSubject() Subject { if !strings.Contains(string(s), "$") { return Subject(s) } bldr := strings.Builder{} tokens := strings.Split(string(s), ".") for i, tk := range tokens { convert := false if len(tk) > 1 && tk[0] == '$' { if _, err := strconv.Atoi(tk[1:]); err == nil { convert = true } } if convert { bldr.WriteString("*") } else { bldr.WriteString(tk) } if i != len(tokens)-1 { bldr.WriteString(".") } } return Subject(bldr.String()) } // Subject is a string that represents a NATS subject type Subject string // Validate checks that a subject string is valid, ie not empty and without spaces func (s Subject) Validate(vr *ValidationResults) { v := string(s) if v == "" { vr.AddError("subject cannot be empty") // No other checks after that make sense return } if strings.Contains(v, " ") { vr.AddError("subject %q cannot have spaces", v) } if v[0] == '.' || v[len(v)-1] == '.' { vr.AddError("subject %q cannot start or end with a `.`", v) } if strings.Contains(v, "..") { vr.AddError("subject %q cannot contain consecutive `.`", v) } } func (s Subject) countTokenWildcards() int { v := string(s) if v == "*" { return 1 } cnt := 0 for _, t := range strings.Split(v, ".") { if t == "*" { cnt++ } } return cnt } // HasWildCards is used to check if a subject contains a > or * func (s Subject) HasWildCards() bool { v := string(s) return strings.HasSuffix(v, ".>") || strings.Contains(v, ".*.") || strings.HasSuffix(v, ".*") || strings.HasPrefix(v, "*.") || v == "*" || v == ">" } // IsContainedIn does a simple test to see if the subject is contained in another subject func (s Subject) IsContainedIn(other Subject) bool { otherArray := strings.Split(string(other), ".") myArray := strings.Split(string(s), ".") if len(myArray) > len(otherArray) && otherArray[len(otherArray)-1] != ">" { return false } if len(myArray) < len(otherArray) { return false } for ind, tok := range otherArray { myTok := myArray[ind] if ind == len(otherArray)-1 && tok == ">" { return true } if tok != myTok && tok != "*" { return false } } return true } // TimeRange is used to represent a start and end time type TimeRange struct { Start string `json:"start,omitempty"` End string `json:"end,omitempty"` } // Validate checks the values in a time range struct func (tr *TimeRange) Validate(vr *ValidationResults) { format := "15:04:05" if tr.Start == "" { vr.AddError("time ranges start must contain a start") } else { _, err := time.Parse(format, tr.Start) if err != nil { vr.AddError("start in time range is invalid %q", tr.Start) } } if tr.End == "" { vr.AddError("time ranges end must contain an end") } else { _, err := time.Parse(format, tr.End) if err != nil { vr.AddError("end in time range is invalid %q", tr.End) } } } // Src is a comma separated list of CIDR specifications type UserLimits struct { Src CIDRList `json:"src,omitempty"` Times []TimeRange `json:"times,omitempty"` Locale string `json:"times_location,omitempty"` } func (u *UserLimits) Empty() bool { return reflect.DeepEqual(*u, UserLimits{}) } func (u *UserLimits) IsUnlimited() bool { return len(u.Src) == 0 && len(u.Times) == 0 } // Limits are used to control acccess for users and importing accounts type Limits struct { UserLimits NatsLimits } func (l *Limits) IsUnlimited() bool { return l.UserLimits.IsUnlimited() && l.NatsLimits.IsUnlimited() } // Validate checks the values in a limit struct func (l *Limits) Validate(vr *ValidationResults) { if len(l.Src) != 0 { for _, cidr := range l.Src { _, ipNet, err := net.ParseCIDR(cidr) if err != nil || ipNet == nil { vr.AddError("invalid cidr %q in user src limits", cidr) } } } if len(l.Times) > 0 { for _, t := range l.Times { t.Validate(vr) } } if l.Locale != "" { if _, err := time.LoadLocation(l.Locale); err != nil { vr.AddError("could not parse iana time zone by name: %v", err) } } } // Permission defines allow/deny subjects type Permission struct { Allow StringList `json:"allow,omitempty"` Deny StringList `json:"deny,omitempty"` } func (p *Permission) Empty() bool { return len(p.Allow) == 0 && len(p.Deny) == 0 } func checkPermission(vr *ValidationResults, subj string, permitQueue bool) { tk := strings.Split(subj, " ") switch len(tk) { case 1: Subject(tk[0]).Validate(vr) case 2: Subject(tk[0]).Validate(vr) Subject(tk[1]).Validate(vr) if !permitQueue { vr.AddError(`Permission Subject "%s" is not allowed to contain queue`, subj) } default: vr.AddError(`Permission Subject "%s" contains too many spaces`, subj) } } // Validate the allow, deny elements of a permission func (p *Permission) Validate(vr *ValidationResults, permitQueue bool) { for _, subj := range p.Allow { checkPermission(vr, subj, permitQueue) } for _, subj := range p.Deny { checkPermission(vr, subj, permitQueue) } } // ResponsePermission can be used to allow responses to any reply subject // that is received on a valid subscription. type ResponsePermission struct { MaxMsgs int `json:"max"` Expires time.Duration `json:"ttl"` } // Validate the response permission. func (p *ResponsePermission) Validate(_ *ValidationResults) { // Any values can be valid for now. } // Permissions are used to restrict subject access, either on a user or for everyone on a server by default type Permissions struct { Pub Permission `json:"pub,omitempty"` Sub Permission `json:"sub,omitempty"` Resp *ResponsePermission `json:"resp,omitempty"` } // Validate the pub and sub fields in the permissions list func (p *Permissions) Validate(vr *ValidationResults) { if p.Resp != nil { p.Resp.Validate(vr) } p.Sub.Validate(vr, true) p.Pub.Validate(vr, false) } // StringList is a wrapper for an array of strings type StringList []string // Contains returns true if the list contains the string func (u *StringList) Contains(p string) bool { for _, t := range *u { if t == p { return true } } return false } // Add appends 1 or more strings to a list func (u *StringList) Add(p ...string) { for _, v := range p { if !u.Contains(v) && v != "" { *u = append(*u, v) } } } // Remove removes 1 or more strings from a list func (u *StringList) Remove(p ...string) { for _, v := range p { for i, t := range *u { if t == v { a := *u *u = append(a[:i], a[i+1:]...) break } } } } // TagList is a unique array of lower case strings // All tag list methods lower case the strings in the arguments type TagList []string // Contains returns true if the list contains the tags func (u *TagList) Contains(p string) bool { p = strings.ToLower(strings.TrimSpace(p)) for _, t := range *u { if t == p { return true } } return false } // Add appends 1 or more tags to a list func (u *TagList) Add(p ...string) { for _, v := range p { v = strings.ToLower(strings.TrimSpace(v)) if !u.Contains(v) && v != "" { *u = append(*u, v) } } } // Remove removes 1 or more tags from a list func (u *TagList) Remove(p ...string) { for _, v := range p { v = strings.ToLower(strings.TrimSpace(v)) for i, t := range *u { if t == v { a := *u *u = append(a[:i], a[i+1:]...) break } } } } type CIDRList TagList func (c *CIDRList) Contains(p string) bool { return (*TagList)(c).Contains(p) } func (c *CIDRList) Add(p ...string) { (*TagList)(c).Add(p...) } func (c *CIDRList) Remove(p ...string) { (*TagList)(c).Remove(p...) } func (c *CIDRList) Set(values string) { *c = CIDRList{} c.Add(strings.Split(strings.ToLower(values), ",")...) } func (c *CIDRList) UnmarshalJSON(body []byte) (err error) { // parse either as array of strings or comma separate list var request []string var list string if err := json.Unmarshal(body, &request); err == nil { *c = request return nil } else if err := json.Unmarshal(body, &list); err == nil { c.Set(list) return nil } else { return err } } jwt-2.7.3/v2/types_test.go000066400000000000000000000236451472706253100154310ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "crypto/rand" "os" "regexp" "strings" "testing" ) func TestVersion(t *testing.T) { // Semantic versioning verRe := regexp.MustCompile(`\d+.\d+.\d+(-\S+)?`) if !verRe.MatchString(Version) { t.Fatalf("Version not compatible with semantic versioning: %q", Version) } } func TestVersionMatchesTag(t *testing.T) { tag := os.Getenv("TRAVIS_TAG") if tag == "" { t.SkipNow() } // We expect a tag of the form vX.Y.Z. If that's not the case, // we need someone to have a look. So fail if first letter is not // a `v` if len(tag) < 2 || tag[0] != 'v' { t.Fatalf("Expect tag to start with `v`, tag is: %s", tag) } // Look only at tag from current 'v', that is v1 for this file. if tag[1] != '2' { // Ignore, it is not a v2 tag. return } // Strip the `v` from the tag for the version comparison. if Version != tag[1:] { t.Fatalf("Version (%s) does not match tag (%s)", Version, tag[1:]) } } func TestTimeRangeValidation(t *testing.T) { tr := TimeRange{ Start: "hello", End: "03:15:00", } vr := CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad start should be invalid") } if !strings.Contains(vr.Issues[0].Error(), tr.Start) { t.Error("error should contain the faulty value") } tr = TimeRange{ Start: "15:43:22", End: "27:11:11", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad end should be invalid") } if !strings.Contains(vr.Issues[0].Error(), tr.End) { t.Error("error should contain the faulty value") } tr = TimeRange{ Start: "", End: "03:15:00", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad start should be invalid") } tr = TimeRange{ Start: "15:43:22", End: "", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad end should be invalid") } } func TestTagList(t *testing.T) { tags := TagList{} tags.Add("one") AssertEquals(true, tags.Contains("one"), t) AssertEquals(true, tags.Contains("ONE"), t) AssertEquals("one", tags[0], t) tags.Add("TWO") AssertEquals(true, tags.Contains("two"), t) AssertEquals(true, tags.Contains("TWO"), t) AssertEquals("two", tags[1], t) tags.Remove("ONE") AssertEquals("two", tags[0], t) AssertEquals(false, tags.Contains("one"), t) AssertEquals(false, tags.Contains("ONE"), t) } func TestStringList(t *testing.T) { slist := StringList{} slist.Add("one") AssertEquals(true, slist.Contains("one"), t) AssertEquals(false, slist.Contains("ONE"), t) AssertEquals("one", slist[0], t) slist.Add("TWO") AssertEquals(false, slist.Contains("two"), t) AssertEquals(true, slist.Contains("TWO"), t) AssertEquals("TWO", slist[1], t) slist.Remove("ONE") AssertEquals("one", slist[0], t) AssertEquals(true, slist.Contains("one"), t) AssertEquals(false, slist.Contains("ONE"), t) slist.Add("ONE") AssertEquals(true, slist.Contains("one"), t) AssertEquals(true, slist.Contains("ONE"), t) AssertEquals(3, len(slist), t) slist.Remove("one") AssertEquals("TWO", slist[0], t) AssertEquals(false, slist.Contains("one"), t) AssertEquals(true, slist.Contains("ONE"), t) } func TestSubjectValid(t *testing.T) { var s Subject vr := CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Empty string is not a valid subjects") } s = "has spaces" vr = CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Subjects cannot contain spaces") } s = "has.spa ces.and.tokens" vr = CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Subjects cannot have spaces") } s = ".start.with.dot" vr = CreateValidationResults() s.Validate(vr) if vr.IsEmpty() || !strings.Contains(vr.Issues[0].Description, "start or end with a `.`") { t.Fatalf("Did not get expected failure: %+v", vr.Issues) } s = "end.with.dot." vr = CreateValidationResults() s.Validate(vr) if vr.IsEmpty() || !strings.Contains(vr.Issues[0].Description, "start or end with a `.`") { t.Fatalf("Did not get expected failure: %+v", vr.Issues) } s = "consecutive..dot" vr = CreateValidationResults() s.Validate(vr) if vr.IsEmpty() || !strings.Contains(vr.Issues[0].Description, "consecutive `.`") { t.Fatalf("Did not get expected failure: %+v", vr.Issues) } s = "one" vr = CreateValidationResults() s.Validate(vr) if !vr.IsEmpty() { t.Fatalf("%s is a valid subject", s) } s = "one.two.three" vr = CreateValidationResults() s.Validate(vr) if !vr.IsEmpty() { t.Fatalf("%s is a valid subject", s) } } func TestSubjectHasWildCards(t *testing.T) { s := Subject("one") AssertEquals(false, s.HasWildCards(), t) s = "one.two.three" AssertEquals(false, s.HasWildCards(), t) s = "*" AssertEquals(true, s.HasWildCards(), t) s = "one.*.three" AssertEquals(true, s.HasWildCards(), t) s = "*.two.three" AssertEquals(true, s.HasWildCards(), t) s = "one.two.*" AssertEquals(true, s.HasWildCards(), t) s = "one.>" AssertEquals(true, s.HasWildCards(), t) s = "one.two.>" AssertEquals(true, s.HasWildCards(), t) s = ">" AssertEquals(true, s.HasWildCards(), t) } func TestSubjectContainment(t *testing.T) { var s Subject var o Subject s = "one.two.three" o = "one.*.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.*.three" o = "one.*.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.*.three" o = "one.two.three" AssertEquals(false, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two.*" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.*.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "*.two.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two.>" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.>" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = ">" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two" AssertEquals(false, s.IsContainedIn(o), t) s = "one" o = "one.two" AssertEquals(false, s.IsContainedIn(o), t) } func TestPermissions_Validate(t *testing.T) { p := Permissions{ Pub: Permission{}, Sub: Permission{}, Resp: nil, } vr := ValidationResults{} resetAndValidate := func() { vr = ValidationResults{} p.Validate(&vr) } resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Resp = &ResponsePermission{ MaxMsgs: 0, Expires: 0, } resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Pub.Allow.Add("foo") p.Pub.Deny.Add("bar") resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Pub.Allow.Add("foo queue") p.Pub.Deny.Add("bar queue") resetAndValidate() AssertTrue(!vr.IsEmpty(), t) AssertTrue(vr.IsBlocking(false), t) AssertTrue(len(vr.Errors()) == 2, t) p.Pub = Permission{} p.Sub.Allow.Add("1") p.Sub.Deny.Add("2") resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Sub.Allow.Add("3 queue") p.Sub.Deny.Add("4 queue") resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Sub.Allow.Add("5.* queue.*.foo") p.Sub.Deny.Add("6.* queue.*.bar") resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Sub.Allow.Add("7.> queue.>") p.Sub.Deny.Add("8.> queue.>") resetAndValidate() AssertTrue(vr.IsEmpty(), t) p.Sub.Allow.Add("9 too many spaces") p.Sub.Deny.Add("0 too many spaces") resetAndValidate() AssertTrue(!vr.IsEmpty(), t) AssertTrue(vr.IsBlocking(false), t) AssertTrue(len(vr.Errors()) == 2, t) } func TestRenamingSubject_ToSubject(t *testing.T) { AssertEquals(RenamingSubject("foo.$2.$1.bar").ToSubject(), Subject("foo.*.*.bar"), t) AssertEquals(RenamingSubject("foo.*.bar").ToSubject(), Subject("foo.*.bar"), t) AssertEquals(RenamingSubject("foo.$2.*.bar").ToSubject(), Subject("foo.*.*.bar"), t) } func TestRenamigSubject_Validate(t *testing.T) { for from, to := range map[string]string{ "foo": ">", "bar": "*", "foo.*": "*.*", "foo.>": "*.*", "bar.>": "*.>", "bar.*.*>": "*.>", "*.bar": "$2", } { vr := ValidationResults{} RenamingSubject(to).Validate(Subject(from), &vr) if !vr.IsBlocking(false) { t.Fatalf("expected blocking issue %q:%q", to, from) } } for from, to := range map[string]string{ "foo": "bar", "foo.bar": "baz", "x": "x.y.z", ">": "foo.>", "*": "$1.foo", "*.*": "$1.foo.$2", "*.bar": "$1", } { vr := ValidationResults{} RenamingSubject(to).Validate(Subject(from), &vr) if !vr.IsEmpty() { t.Fatalf("expected no issue %q:%q got: %v", to, from, vr.Issues) } } } func TestInvalidInfo(t *testing.T) { tooLong := [MaxInfoLength + 21]byte{} rand.Read(tooLong[:]) for _, info := range []Info{{ Description: "", InfoURL: "/bad", }, { Description: string(tooLong[:]), InfoURL: "http://localhost/foo/bar", }, { Description: "", InfoURL: `http://1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901 23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890`, }} { vr := CreateValidationResults() info.Validate(vr) if vr.IsEmpty() { t.Errorf("info should not validate cleanly") } if !vr.IsBlocking(true) { t.Errorf("invalid info needs to be blocking") } } } jwt-2.7.3/v2/user_claims.go000066400000000000000000000104151472706253100155230ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "errors" "reflect" "github.com/nats-io/nkeys" ) const ( ConnectionTypeStandard = "STANDARD" ConnectionTypeWebsocket = "WEBSOCKET" ConnectionTypeLeafnode = "LEAFNODE" ConnectionTypeLeafnodeWS = "LEAFNODE_WS" ConnectionTypeMqtt = "MQTT" ConnectionTypeMqttWS = "MQTT_WS" ConnectionTypeInProcess = "IN_PROCESS" ) type UserPermissionLimits struct { Permissions Limits BearerToken bool `json:"bearer_token,omitempty"` AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"` } // User defines the user specific data in a user JWT type User struct { UserPermissionLimits // IssuerAccount stores the public key for the account the issuer represents. // When set, the claim was issued by a signing key. IssuerAccount string `json:"issuer_account,omitempty"` GenericFields } // Validate checks the permissions and limits in a User jwt func (u *User) Validate(vr *ValidationResults) { u.Permissions.Validate(vr) u.Limits.Validate(vr) // When BearerToken is true server will ignore any nonce-signing verification } // UserClaims defines a user JWT type UserClaims struct { ClaimsData User `json:"nats,omitempty"` } // NewUserClaims creates a user JWT with the specific subject/public key func NewUserClaims(subject string) *UserClaims { if subject == "" { return nil } c := &UserClaims{} c.Subject = subject c.Limits = Limits{ UserLimits{CIDRList{}, nil, ""}, NatsLimits{NoLimit, NoLimit, NoLimit}, } return c } func (u *UserClaims) SetScoped(t bool) { if t { u.UserPermissionLimits = UserPermissionLimits{} } else { u.Limits = Limits{ UserLimits{CIDRList{}, nil, ""}, NatsLimits{NoLimit, NoLimit, NoLimit}, } } } func (u *UserClaims) HasEmptyPermissions() bool { return reflect.DeepEqual(u.UserPermissionLimits, UserPermissionLimits{}) } // Encode tries to turn the user claims into a JWT string func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) { return u.EncodeWithSigner(pair, nil) } func (u *UserClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicUserKey(u.Subject) { return "", errors.New("expected subject to be user public key") } u.Type = UserClaim return u.ClaimsData.encode(pair, u, fn) } // DecodeUserClaims tries to parse a user claims from a JWT string func DecodeUserClaims(token string) (*UserClaims, error) { claims, err := Decode(token) if err != nil { return nil, err } ac, ok := claims.(*UserClaims) if !ok { return nil, errors.New("not user claim") } return ac, nil } func (u *UserClaims) ClaimType() ClaimType { return u.Type } // Validate checks the generic and specific parts of the user jwt func (u *UserClaims) Validate(vr *ValidationResults) { u.ClaimsData.Validate(vr) u.User.Validate(vr) if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) { vr.AddError("account_id is not an account public key") } } // ExpectedPrefixes defines the types that can encode a user JWT, account func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount} } // Claims returns the generic data from a user jwt func (u *UserClaims) Claims() *ClaimsData { return &u.ClaimsData } // Payload returns the user specific data from a user JWT func (u *UserClaims) Payload() interface{} { return &u.User } func (u *UserClaims) String() string { return u.ClaimsData.String(u) } func (u *UserClaims) updateVersion() { u.GenericFields.Version = libVersion } // IsBearerToken returns true if nonce-signing requirements should be skipped func (u *UserClaims) IsBearerToken() bool { return u.BearerToken } func (u *UserClaims) GetTags() TagList { return u.User.Tags } jwt-2.7.3/v2/user_claims_test.go000066400000000000000000000260221472706253100165630ustar00rootroot00000000000000/* * Copyright 2018-2024 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewUserClaims(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) if !uc.Limits.IsUnlimited() { t.Fatal("unlimited after creation") } uc.Expires = time.Now().Add(time.Hour).Unix() uJwt := encode(uc, akp, t) uc2, err := DecodeUserClaims(uJwt) if err != nil { t.Fatal("failed to decode uc", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestUserClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Expires = time.Now().Add(time.Hour).Unix() uJwt := encode(uc, akp, t) temp, err := DecodeGeneric(uJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeUserClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode user signed by %q", i.name) t.Fail() } } } func TestUserSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), true}, } for _, i := range inputs { c := NewUserClaims(publicKey(i.kp, t)) _, err := c.Encode(createAccountNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode user with with %q subject", i.name) t.Fail() } } } func TestNewNilUserClaim(t *testing.T) { v := NewUserClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestUserType(t *testing.T) { c := NewUserClaims(publicKey(createUserNKey(t), t)) s := encode(c, createAccountNKey(t), t) u, err := DecodeUserClaims(s) if err != nil { t.Fatalf("failed to decode user claim: %v", err) } if UserClaim != u.Type { t.Fatalf("user type is unexpected %q", u.Type) } } func TestSubjects(t *testing.T) { s := StringList{} if len(s) != 0 { t.Fatalf("expected len 0") } if s.Contains("a") { t.Fatalf("didn't expect 'a'") } s.Add("a") if !s.Contains("a") { t.Fatalf("expected 'a'") } s.Remove("a") if s.Contains("a") { t.Fatalf("didn't expect 'a' after removing") } } func TestUserValidation(t *testing.T) { ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Permissions.Pub.Allow.Add("a") uc.Permissions.Pub.Deny.Add("b") uc.Permissions.Sub.Allow.Add("a") uc.Permissions.Sub.Deny.Add("b") uc.Permissions.Resp = &ResponsePermission{ MaxMsgs: 10, Expires: 50 * time.Minute, } uc.Limits.Payload = 10 uc.Limits.Src.Set("192.0.2.0/24") uc.Limits.Times = []TimeRange{ { Start: "01:15:00", End: "03:15:00", }, { Start: "06:15:00", End: "09:15:00", }, } uc.Limits.Locale = "Europe/Berlin" vr := CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("valid user permissions should be valid") } uc.Limits.Src.Set("hello world") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Payload = 10 uc.Limits.Src.Set("hello world") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } tr := TimeRange{ Start: "hello", End: "03:15:00", } uc.Limits.Src.Set("192.0.2.0/24") uc.Limits.Times = append(uc.Limits.Times, tr) vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Times = []TimeRange{{ Start: "02:15:00", End: "03:15:00", }} uc.Limits.Locale = "foo" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad location should be invalid") } } func TestUserAccountID(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) a2kp := createAccountNKey(t) ac := NewAccountClaims(apk) ac.SigningKeys.Add(publicKey(a2kp, t)) token, err := ac.Encode(akp) if err != nil { t.Fatal(err) } ac, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } uc := NewUserClaims(publicKey(createUserNKey(t), t)) uc.IssuerAccount = apk userToken, err := uc.Encode(a2kp) if err != nil { t.Fatal(err) } uc, err = DecodeUserClaims(userToken) if err != nil { t.Fatal(err) } if uc.IssuerAccount != apk { t.Fatalf("expected AccountID to be set to %s - got %s", apk, uc.IssuerAccount) } signed := ac.DidSign(uc) if !signed { t.Fatal("expected user signed by account") } } func TestUserAccountIDValidation(t *testing.T) { uc := NewUserClaims(publicKey(createUserNKey(t), t)) uc.IssuerAccount = publicKey(createAccountNKey(t), t) var vr ValidationResults uc.Validate(&vr) if len(vr.Issues) != 0 { t.Fatal("expected no issues") } uc.IssuerAccount = publicKey(createUserNKey(t), t) uc.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected validation issues") } } func TestSourceNetworkValidation(t *testing.T) { ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Limits.Src = CIDRList{"192.0.2.0/24"} vr := CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = CIDRList{"192.0.2.0/24"} vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = CIDRList{"192.0.2.0/24", "2001:db8:a0b:12f0::1/32"} vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src.Set("192.0.2.0/24, \t2001:db8:a0b:12f0::1/32 , 192.168.1.1/1") vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src.Set("192.0.2.0/24,2001:db8:a0b:12f0::1/32,192.168.1.1/1") vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = CIDRList{"foo"} vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 { t.Error("limits should be invalid") } uc.Limits.Src = CIDRList{"192.0.2.0/24", "foo"} vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 { t.Error("limits should be invalid") } uc.Limits.Src = CIDRList{"bloo", "foo"} vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 2 { t.Error("limits should be invalid") } } func TestUserAllowedConnectionTypes(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.AllowedConnectionTypes.Add(ConnectionTypeStandard) uc.AllowedConnectionTypes.Add(ConnectionTypeWebsocket) uc.AllowedConnectionTypes.Add(ConnectionTypeLeafnode) uc.AllowedConnectionTypes.Add(ConnectionTypeLeafnodeWS) uc.AllowedConnectionTypes.Add(ConnectionTypeMqtt) uc.AllowedConnectionTypes.Add(ConnectionTypeMqttWS) uc.AllowedConnectionTypes.Add(ConnectionTypeInProcess) uJwt := encode(uc, akp, t) uc2, err := DecodeUserClaims(uJwt) if err != nil { t.Fatal("failed to decode uc", err) } AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeStandard), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeWebsocket), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeLeafnode), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeLeafnodeWS), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeMqtt), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeMqttWS), t) AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeInProcess), t) } func TestUserClaimRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) u := publicKey(createUserNKey(t), t) aminAgo := time.Now().Add(-time.Minute) if account.Revocations.IsRevoked(u, aminAgo) { t.Fatal("shouldn't be revoked") } account.RevokeAt(u, aminAgo) if !account.Revocations.IsRevoked(u, aminAgo) { t.Fatal("should be revoked") } u2 := publicKey(createUserNKey(t), t) if account.Revocations.IsRevoked(u2, aminAgo) { t.Fatal("should not be revoked") } account.RevokeAt("*", aminAgo) if !account.Revocations.IsRevoked(u2, time.Now().Add(-time.Hour)) { t.Fatal("should be revoked") } vr := ValidationResults{} account.Validate(&vr) if !vr.IsEmpty() { t.Fatal("account validation shouldn't have failed") } } func TestUserClaims_GetTags(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) upk := publicKey(ukp, t) uc := NewUserClaims(upk) uc.User.Tags.Add("foo", "bar") tags := uc.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } token, err := uc.Encode(akp) if err != nil { t.Fatal("error encoding") } uc, err = DecodeUserClaims(token) if err != nil { t.Fatal("error decoding") } tags = uc.GetTags() if len(tags) != 2 { t.Fatal("expected 2 tags") } if tags[0] != "foo" { t.Fatal("expected tag foo") } if tags[1] != "bar" { t.Fatal("expected tag bar") } } func TestUserClaimsSignerFn(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) if !uc.Limits.IsUnlimited() { t.Fatal("unlimited after creation") } ok := false tok, err := uc.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { ok = true return akp.Sign(data) }) if err != nil { t.Fatal("error encoding") } if !ok { t.Fatal("fn didn't sign") } uc2, err := DecodeUserClaims(tok) if err != nil { t.Fatal("failed to decode uc", err) } vr := CreateValidationResults() uc2.Validate(vr) if !vr.IsEmpty() { t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) } } jwt-2.7.3/v2/util_test.go000066400000000000000000000054151472706253100152350ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "runtime" "strings" "testing" "github.com/nats-io/nkeys" ) func Trace(message string) string { lines := make([]string, 0, 32) err := errors.New(message) msg := err.Error() lines = append(lines, msg) for i := 2; true; i++ { _, file, line, ok := runtime.Caller(i) if !ok { break } msg := fmt.Sprintf("%s:%d", file, line) lines = append(lines, msg) } return strings.Join(lines, "\n") } func AssertEquals(expected, v interface{}, t *testing.T) { if expected != v { t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) } } func AssertNil(v interface{}, t *testing.T) { if v != nil { t.FailNow() } } func AssertNoError(err error, t *testing.T) { if err != nil { t.Fatal(err) } } func AssertTrue(condition bool, t *testing.T) { if !condition { t.FailNow() } } func AssertFalse(condition bool, t *testing.T) { if condition { t.FailNow() } } func createAccountNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("error creating account kp", err) } return kp } func createUserNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateUser() if err != nil { t.Fatal("error creating account kp", err) } return kp } func createOperatorNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateOperator() if err != nil { t.Fatal("error creating operator kp", err) } return kp } func createServerNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateServer() if err != nil { t.Fatal("error creating server kp", err) } return kp } func createClusterNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateCluster() if err != nil { t.Fatal("error creating cluster kp", err) } return kp } func publicKey(kp nkeys.KeyPair, t *testing.T) string { pk, err := kp.PublicKey() if err != nil { t.Fatal("error reading public key", err) } return pk } func seedKey(kp nkeys.KeyPair, t *testing.T) []byte { sk, err := kp.Seed() if err != nil { t.Fatal("error reading seed", err) } return sk } func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { s, err := c.Encode(kp) if err != nil { t.Fatal("error encoding claim", err) } return s } jwt-2.7.3/v2/v1compat/000077500000000000000000000000001472706253100144175ustar00rootroot00000000000000jwt-2.7.3/v2/v1compat/Makefile000066400000000000000000000003661472706253100160640ustar00rootroot00000000000000.PHONY: test cover build: go build test: ../../../scripts/test.sh fmt: gofmt -w -s *.go go mod tidy cd v2/ gofmt -w -s *.go go mod tidy cover: go test -v -covermode=count -coverprofile=coverage.out go tool cover -html=coverage.out jwt-2.7.3/v2/v1compat/account_claims.go000066400000000000000000000174551472706253100177460ustar00rootroot00000000000000/* * Copyright 2018-2023 The NATS Authors * 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. */ package jwt import ( "errors" "sort" "time" "github.com/nats-io/nkeys" ) // NoLimit is used to indicate a limit field is unlimited in value. const NoLimit = -1 // OperatorLimits are used to limit access by an account type OperatorLimits struct { Subs int64 `json:"subs,omitempty"` // Max number of subscriptions Conn int64 `json:"conn,omitempty"` // Max number of active connections LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections Imports int64 `json:"imports,omitempty"` // Max number of imports Exports int64 `json:"exports,omitempty"` // Max number of exports Data int64 `json:"data,omitempty"` // Max number of bytes Payload int64 `json:"payload,omitempty"` // Max message payload WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports } // IsEmpty returns true if all of the limits are 0/false. func (o *OperatorLimits) IsEmpty() bool { return *o == OperatorLimits{} } // IsUnlimited returns true if all limits are func (o *OperatorLimits) IsUnlimited() bool { return *o == OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true} } // Validate checks that the operator limits contain valid values func (o *OperatorLimits) Validate(vr *ValidationResults) { // negative values mean unlimited, so all numbers are valid } // Account holds account specific claims data type Account struct { Imports Imports `json:"imports,omitempty"` Exports Exports `json:"exports,omitempty"` Identities []Identity `json:"identity,omitempty"` Limits OperatorLimits `json:"limits,omitempty"` SigningKeys StringList `json:"signing_keys,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` } // Validate checks if the account is valid, based on the wrapper func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) { a.Imports.Validate(acct.Subject, vr) a.Exports.Validate(vr) a.Limits.Validate(vr) for _, i := range a.Identities { i.Validate(vr) } if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports { vr.AddError("the account contains more imports than allowed by the operator") } // Check Imports and Exports for limit violations. if a.Limits.Imports != NoLimit { if int64(len(a.Imports)) > a.Limits.Imports { vr.AddError("the account contains more imports than allowed by the operator") } } if a.Limits.Exports != NoLimit { if int64(len(a.Exports)) > a.Limits.Exports { vr.AddError("the account contains more exports than allowed by the operator") } // Check for wildcard restrictions if !a.Limits.WildcardExports { for _, ex := range a.Exports { if ex.Subject.HasWildCards() { vr.AddError("the account contains wildcard exports that are not allowed by the operator") } } } } for _, k := range a.SigningKeys { if !nkeys.IsValidPublicAccountKey(k) { vr.AddError("%s is not an account public key", k) } } } // AccountClaims defines the body of an account JWT type AccountClaims struct { ClaimsData Account `json:"nats,omitempty"` } // NewAccountClaims creates a new account JWT func NewAccountClaims(subject string) *AccountClaims { if subject == "" { return nil } c := &AccountClaims{} // Set to unlimited to start. We do it this way so we get compiler // errors if we add to the OperatorLimits. c.Limits = OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true} c.Subject = subject return c } // Encode converts account claims into a JWT string func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicAccountKey(a.Subject) { return "", errors.New("expected subject to be account public key") } sort.Sort(a.Exports) sort.Sort(a.Imports) a.ClaimsData.Type = AccountClaim return a.ClaimsData.Encode(pair, a) } // DecodeAccountClaims decodes account claims from a JWT string func DecodeAccountClaims(token string) (*AccountClaims, error) { v := AccountClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } func (a *AccountClaims) String() string { return a.ClaimsData.String(a) } // Payload pulls the accounts specific payload out of the claims func (a *AccountClaims) Payload() interface{} { return &a.Account } // Validate checks the accounts contents func (a *AccountClaims) Validate(vr *ValidationResults) { a.ClaimsData.Validate(vr) a.Account.Validate(a, vr) if nkeys.IsValidPublicAccountKey(a.ClaimsData.Issuer) { if len(a.Identities) > 0 { vr.AddWarning("self-signed account JWTs shouldn't contain identity proofs") } if !a.Limits.IsEmpty() { vr.AddWarning("self-signed account JWTs shouldn't contain operator limits") } } } // ExpectedPrefixes defines the types that can encode an account jwt, account and operator func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} } // Claims returns the accounts claims data func (a *AccountClaims) Claims() *ClaimsData { return &a.ClaimsData } // DidSign checks the claims against the account's public key and its signing keys func (a *AccountClaims) DidSign(c Claims) bool { if c != nil { issuer := c.Claims().Issuer if issuer == a.Subject { return true } uc, ok := c.(*UserClaims) if ok && uc.IssuerAccount == a.Subject { return a.SigningKeys.Contains(issuer) } at, ok := c.(*ActivationClaims) if ok && at.IssuerAccount == a.Subject { return a.SigningKeys.Contains(issuer) } } return false } // Revoke enters a revocation by publickey using time.Now(). func (a *AccountClaims) Revoke(pubKey string) { a.RevokeAt(pubKey, time.Now()) } // RevokeAt enters a revocation by public key and timestamp into this account // This will revoke all jwt issued for pubKey, prior to timestamp // If there is already a revocation for this public key that is newer, it is kept. func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) { if a.Revocations == nil { a.Revocations = RevocationList{} } a.Revocations.Revoke(pubKey, timestamp) } // ClearRevocation removes any revocation for the public key func (a *AccountClaims) ClearRevocation(pubKey string) { a.Revocations.ClearRevocation(pubKey) } // IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in. // Generally this method is called with the subject and issue time of the jwt to be tested. // DO NOT pass time.Now(), it will not produce a stable/expected response. // The value is expected to be a public key or "*" (means all public keys) func (a *AccountClaims) IsRevokedAt(pubKey string, timestamp time.Time) bool { return a.Revocations.IsRevoked(pubKey, timestamp) } // IsRevoked does not perform a valid check. Use IsRevokedAt instead. func (a *AccountClaims) IsRevoked(_ string) bool { return true } // IsClaimRevoked checks if the account revoked the claim passed in. // Invalid claims (nil, no Subject or IssuedAt) will return true. func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool { if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { return true } return a.Revocations.IsRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) } jwt-2.7.3/v2/v1compat/account_claims_test.go000066400000000000000000000342721472706253100210010ustar00rootroot00000000000000/* * Copyright 2018-2023 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewAccountClaims(t *testing.T) { akp := createAccountNKey(t) akp2 := createAccountNKey(t) apk := publicKey(akp, t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Max = 1024 * 1024 activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream actJWT := encode(activation, akp2, t) account := NewAccountClaims(apk) if !account.Limits.IsUnlimited() { t.Fatalf("Expected unlimited operator limits") } account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).UTC().Unix() account.Imports = Imports{} account.Imports.Add(&Import{Subject: "test", Name: "test import", Account: apk2, Token: actJWT, To: "my", Type: Stream}) vr := CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() { t.Fatal("Valid account will have no validation results") } actJwt := encode(account, akp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) AssertEquals(account2.IsSelfSigned(), true, t) AssertEquals(account2.Claims() != nil, true, t) AssertEquals(account2.Payload() != nil, true, t) } func TestAccountCanSignOperatorLimits(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() account.Limits.Conn = 10 account.Limits.LeafNodeConn = 2 _, err := account.Encode(akp) if err != nil { t.Fatal("account should not be able to encode operator limits", err) } } func TestAccountCanSignIdentities(t *testing.T) { // don't block encoding!!! akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() account.Identities = []Identity{ { ID: "stephen", Proof: "yougotit", }, } _, err := account.Encode(akp) if err != nil { t.Fatal("account should not be able to encode identities", err) } } func TestOperatorCanSignClaims(t *testing.T) { akp := createAccountNKey(t) okp := createOperatorNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() account.Limits.Conn = 1 account.Limits.LeafNodeConn = 4 account.Identities = []Identity{ { ID: "stephen", Proof: "yougotit", }, } actJwt := encode(account, okp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) AssertEquals(account2.IsSelfSigned(), false, t) if account2.Limits.Conn != 1 { t.Fatalf("Expected Limits.Conn == 1, got %d", account2.Limits.Conn) } if account2.Limits.LeafNodeConn != 4 { t.Fatalf("Expected Limits.Conn == 4, got %d", account2.Limits.LeafNodeConn) } } func TestInvalidAccountClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ac := NewAccountClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeAccountClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestInvalidAccountSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { pk := publicKey(i.kp, t) var err error c := NewAccountClaims(pk) if i.ok && err != nil { t.Fatalf("error encoding activation: %v", err) } _, err = c.Encode(i.kp) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode account with with %q subject", i.name) t.Fail() } } } func TestAccountImports(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() actJwt := encode(account, akp, t) account2, err := DecodeAccountClaims(actJwt) if err != nil { t.Fatal("error decoding account jwt", err) } AssertEquals(account.String(), account2.String(), t) } func TestNewNilAccountClaim(t *testing.T) { v := NewAccountClaims("") if v != nil { t.Fatal("expected nil account claim") } } func TestLimitValidationInAccount(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() account.Limits.Conn = 10 account.Limits.Imports = 10 account.Limits.Exports = 10 account.Limits.Data = 1024 account.Limits.Payload = 1024 account.Limits.Subs = 10 account.Limits.WildcardExports = true account.Identities = []Identity{ { ID: "stephen", Proof: "yougotit", }, } vr := CreateValidationResults() account.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid account should have no validation issues") } account.Limits.Conn = -1 account.Limits.Imports = -1 account.Limits.Exports = -1 account.Limits.Subs = -1 account.Limits.Data = -1 account.Limits.Payload = -1 vr = CreateValidationResults() account.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid account should have no validation issues") } op := createOperatorNKey(t) opk := publicKey(op, t) account.Issuer = opk vr = CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("operator can encode limits and identity") } account.Identities = nil account.Issuer = apk vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("bad issuer for limits should have non-blocking validation results") } account.Identities = []Identity{ { ID: "stephen", Proof: "yougotit", }, } account.Limits = OperatorLimits{} account.Issuer = apk vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("bad issuer for identities should have non-blocking validation results") } account.Identities = nil account.Issuer = apk vr = CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Fatal("account can encode without limits and identity") } } func TestWildcardExportLimit(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) account.Expires = time.Now().Add(time.Duration(time.Hour * 24 * 365)).Unix() account.Limits.Conn = 10 account.Limits.Imports = 10 account.Limits.Exports = 10 account.Limits.WildcardExports = true account.Exports = Exports{ &Export{Subject: "foo", Type: Stream}, &Export{Subject: "bar.*", Type: Stream}, } vr := CreateValidationResults() account.Validate(vr) if !vr.IsEmpty() { t.Fatal("valid account should have no validation issues") } account.Limits.WildcardExports = false vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Fatal("invalid account should have validation issues") } account.Limits.WildcardExports = true account.Limits.Exports = 1 vr = CreateValidationResults() account.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Fatal("invalid account should have validation issues") } } func TestAccountSigningKeyValidation(t *testing.T) { okp := createOperatorNKey(t) akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2) var vr ValidationResults ac.Validate(&vr) if len(vr.Issues) != 0 { t.Fatal("expected no validation issues") } // try encoding/decoding token, err := ac.Encode(okp) if err != nil { t.Fatal(err) } ac2, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } if len(ac2.SigningKeys) != 1 { t.Fatal("expected claim to have a signing key") } if ac.SigningKeys[0] != apk2 { t.Fatalf("expected signing key to be %s - got %s", apk2, ac.SigningKeys[0]) } bkp := createUserNKey(t) ac.SigningKeys.Add(publicKey(bkp, t)) ac.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected 1 validation issue") } } func TestAccountSignedBy(t *testing.T) { okp := createOperatorNKey(t) akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2) token, err := ac.Encode(okp) if err != nil { t.Fatal(err) } ac2, err := DecodeAccountClaims(token) if err != nil { t.Fatal(err) } if len(ac2.SigningKeys) != 1 { t.Fatal("expected claim to have a signing key") } if ac.SigningKeys[0] != apk2 { t.Fatalf("expected signing key to be %s - got %s", apk2, ac.SigningKeys[0]) } ukp := createUserNKey(t) upk := publicKey(ukp, t) // claim signed by alternate key uc := NewUserClaims(upk) uc.IssuerAccount = apk1 utoken, err := uc.Encode(akp2) if err != nil { t.Fatal(err) } uc2, err := DecodeUserClaims(utoken) if err != nil { t.Fatal(err) } if !ac2.DidSign(uc2) { t.Fatal("failed to verify user claim") } // claim signed by the account pk uc3 := NewUserClaims(upk) utoken2, err := uc3.Encode(akp1) if err != nil { t.Fatal(err) } uc4, err := DecodeUserClaims(utoken2) if err != nil { t.Fatal(err) } if !ac2.DidSign(uc4) { t.Fatal("failed to verify user claim") } } func TestAddRemoveSigningKey(t *testing.T) { akp1 := createAccountNKey(t) apk1 := publicKey(akp1, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) akp3 := createAccountNKey(t) apk3 := publicKey(akp3, t) ac := NewAccountClaims(apk1) ac.SigningKeys.Add(apk2, apk3) if len(ac.SigningKeys) != 2 { t.Fatal("expected 2 signing keys") } ac.SigningKeys.Remove(publicKey(createAccountNKey(t), t)) if len(ac.SigningKeys) != 2 { t.Fatal("expected 2 signing keys") } ac.SigningKeys.Remove(apk2) if len(ac.SigningKeys) != 1 { t.Fatal("expected single signing keys") } } func TestUserRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) ukp := createUserNKey(t) pubKey := publicKey(ukp, t) uc := NewUserClaims(pubKey) uJwt, _ := uc.Encode(akp) uc, err := DecodeUserClaims(uJwt) if err != nil { t.Errorf("Failed to decode user claim: %v", err) } now := time.Now() // test that clear is safe before we add any account.ClearRevocation(pubKey) if account.IsRevokedAt(pubKey, now) { t.Errorf("no revocation was added so is revoked should be false") } account.RevokeAt(pubKey, now.Add(time.Second*100)) if !account.IsRevokedAt(pubKey, now) { t.Errorf("revocation should hold when timestamp is in the future") } if account.IsRevokedAt(pubKey, now.Add(time.Second*150)) { t.Errorf("revocation should time out") } account.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in if !account.IsRevokedAt(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should hold, 100 > 50") } encoded, _ := account.Encode(akp) decoded, _ := DecodeAccountClaims(encoded) if !decoded.IsRevokedAt(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should last across encoding") } account.ClearRevocation(pubKey) if account.IsClaimRevoked(uc) { t.Errorf("revocations should be cleared") } account.RevokeAt(pubKey, now.Add(time.Second*1000)) if !account.IsClaimRevoked(uc) { t.Errorf("revocation be true we revoked in the future") } } func TestUserRevocationAll(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) now := time.Now().Add(time.Second * -10) account.RevokeAt(All, now) before := now.Add(time.Second * -1) if !account.IsRevokedAt("foo", before) { t.Error("foo should have been revoked (before)") } if !account.IsRevokedAt("foo", now) { t.Error("foo should have been revoked (now)") } if account.IsRevokedAt("foo", now.Add(time.Second)) { t.Error("foo should have not been revoked") } } func TestAccountClaims_DidSign(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) skp := createAccountNKey(t) spk := publicKey(skp, t) ac := NewAccountClaims(apk) ac.SigningKeys.Add(spk) upk := publicKey(createUserNKey(t), t) uc := NewUserClaims(upk) tok, err := uc.Encode(akp) if err != nil { t.Fatal("error encoding") } uc, err = DecodeUserClaims(tok) if err != nil { t.Fatal("error decoding") } if !ac.DidSign(uc) { t.Fatal("expected account to have been issued") } uc = NewUserClaims(upk) uc.IssuerAccount = publicKey(createAccountNKey(t), t) tok, err = uc.Encode(skp) if err != nil { t.Fatalf("encode failed %v", err) } uc, err = DecodeUserClaims(tok) if err != nil { t.Fatalf("decode failed %v", err) } if ac.DidSign(uc) { t.Fatal("this is not issued by account A") } } jwt-2.7.3/v2/v1compat/activation_claims.go000066400000000000000000000115441472706253100204440ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "crypto/sha256" "encoding/base32" "errors" "fmt" "strings" "github.com/nats-io/nkeys" ) // Activation defines the custom parts of an activation claim type Activation struct { ImportSubject Subject `json:"subject,omitempty"` ImportType ExportType `json:"type,omitempty"` Limits } // IsService returns true if an Activation is for a service func (a *Activation) IsService() bool { return a.ImportType == Service } // IsStream returns true if an Activation is for a stream func (a *Activation) IsStream() bool { return a.ImportType == Stream } // Validate checks the exports and limits in an activation JWT func (a *Activation) Validate(vr *ValidationResults) { if !a.IsService() && !a.IsStream() { vr.AddError("invalid export type: %q", a.ImportType) } if a.IsService() { if a.ImportSubject.HasWildCards() { vr.AddError("services cannot have wildcard subject: %q", a.ImportSubject) } } a.ImportSubject.Validate(vr) a.Limits.Validate(vr) } // ActivationClaims holds the data specific to an activation JWT type ActivationClaims struct { ClaimsData Activation `json:"nats,omitempty"` // IssuerAccount stores the public key for the account the issuer represents. // When set, the claim was issued by a signing key. IssuerAccount string `json:"issuer_account,omitempty"` } // NewActivationClaims creates a new activation claim with the provided sub func NewActivationClaims(subject string) *ActivationClaims { if subject == "" { return nil } ac := &ActivationClaims{} ac.Subject = subject return ac } // Encode turns an activation claim into a JWT strimg func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) { return "", errors.New("expected subject to be an account") } a.ClaimsData.Type = ActivationClaim return a.ClaimsData.Encode(pair, a) } // DecodeActivationClaims tries to create an activation claim from a JWT string func DecodeActivationClaims(token string) (*ActivationClaims, error) { v := ActivationClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } // Payload returns the activation specific part of the JWT func (a *ActivationClaims) Payload() interface{} { return a.Activation } // Validate checks the claims func (a *ActivationClaims) Validate(vr *ValidationResults) { a.validateWithTimeChecks(vr, true) } // Validate checks the claims func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) { if timeChecks { a.ClaimsData.Validate(vr) } a.Activation.Validate(vr) if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) { vr.AddError("account_id is not an account public key") } } // ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} } // Claims returns the generic part of the JWT func (a *ActivationClaims) Claims() *ClaimsData { return &a.ClaimsData } func (a *ActivationClaims) String() string { return a.ClaimsData.String(a) } // HashID returns a hash of the claims that can be used to identify it. // The hash is calculated by creating a string with // issuerPubKey.subjectPubKey. and constructing the sha-256 hash and base32 encoding that. // is the exported subject, minus any wildcards, so foo.* becomes foo. // the one special case is that if the export start with "*" or is ">" the "_" func (a *ActivationClaims) HashID() (string, error) { if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" { return "", fmt.Errorf("not enough data in the activaion claims to create a hash") } subject := cleanSubject(string(a.ImportSubject)) base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject) h := sha256.New() h.Write([]byte(base)) sha := h.Sum(nil) hash := base32.StdEncoding.EncodeToString(sha) return hash, nil } func cleanSubject(subject string) string { split := strings.Split(subject, ".") cleaned := "" for i, tok := range split { if tok == "*" || tok == ">" { if i == 0 { cleaned = "_" break } cleaned = strings.Join(split[:i], ".") break } } if cleaned == "" { cleaned = subject } return cleaned } jwt-2.7.3/v2/v1compat/activation_claims_test.go000066400000000000000000000261151472706253100215030ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewActivationClaims(t *testing.T) { okp := createOperatorNKey(t) akp := createAccountNKey(t) apk := publicKey(akp, t) activation := NewActivationClaims(apk) activation.Max = 1024 * 1024 activation.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() activation.Limits.Max = 10 activation.Limits.Payload = 10 activation.Limits.Src = "192.0.2.0/24" activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } actJwt := encode(activation, okp, t) activation2, err := DecodeActivationClaims(actJwt) if err != nil { t.Fatal("failed to decode activation", err) } AssertEquals(activation.String(), activation2.String(), t) AssertEquals(activation.Claims() != nil, true, t) AssertEquals(activation.Payload() != nil, true, t) } func TestInvalidActivationTargets(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewActivationClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode user with with %q subject", i.name) t.Fail() } } } func TestInvalidActivationClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ac := NewActivationClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeActivationClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestPublicIsNotValid(t *testing.T) { c := NewActivationClaims("public") _, err := c.Encode(createOperatorNKey(t)) if err == nil { t.Fatal("should not have encoded public activation anymore") } } func TestNilActivationClaim(t *testing.T) { v := NewActivationClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestActivationImportSubjectValidation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportType = Service vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "foo.*" // wildcards are bad vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Error("wildcard service activation should not pass validation") } activation.ImportType = Stream // Stream is ok with wildcards vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "" // empty strings are bad vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Error("empty activation should not pass validation") } activation.ImportSubject = "foo bar" // spaces are bad vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || !vr.IsBlocking(true) { t.Error("spaces in activation should not pass validation") } } func TestActivationValidation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 activation.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() activation.ImportSubject = "foo" activation.Name = "Foo" activation.ImportType = Stream activation.Limits.Max = 10 activation.Limits.Payload = 10 activation.Limits.Src = "192.0.2.0/24" activation.Limits.Times = []TimeRange{ { Start: "01:15:00", End: "03:15:00", }, { Start: "06:15:00", End: "09:15:00", }, } vr := CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "times.*" activation.ImportType = Stream activation.Name = "times" vr = CreateValidationResults() activation.Validate(vr) if !vr.IsEmpty() || vr.IsBlocking(true) { t.Error("valid activation should pass validation") } activation.ImportSubject = "other.*" activation.ImportType = Stream activation.Name = "other" activation.Limits.Max = -1 vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } activation.Limits.Max = 10 activation.Limits.Payload = -1 vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } activation.Limits.Payload = 10 activation.Limits.Src = "hello world" vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } activation.Limits.Payload = 10 activation.Limits.Src = "hello world" vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } tr := TimeRange{ Start: "hello", End: "03:15:00", } activation.Limits.Src = "192.0.2.0/24" activation.Limits.Times = append(activation.Limits.Times, tr) vr = CreateValidationResults() activation.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } } func TestActivationHashIDLimits(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) akp2 := createAccountNKey(t) apk2 := publicKey(akp2, t) activation := NewActivationClaims(apk) activation.Issuer = apk activation.Subject = apk2 _, err := activation.HashID() if err == nil { t.Fatal("activation without subject should fail to hash") } activation.ImportSubject = "times.*" activation.ImportType = Stream activation.Name = "times" hash, err := activation.HashID() if err != nil { t.Fatalf("activation with subject should hash %v", err) } activation2 := NewActivationClaims(apk) activation2.Issuer = apk activation2.Subject = apk2 activation2.ImportSubject = "times.*.bar" activation2.ImportType = Stream activation2.Name = "times" hash2, err := activation2.HashID() if err != nil { t.Fatalf("activation with subject should hash %v", err) } if hash != hash2 { t.Fatal("subjects should be stripped to create hash") } } func TestActivationClaimAccountIDValidation(t *testing.T) { issuerAccountKP := createAccountNKey(t) issuerAccountPK := publicKey(issuerAccountKP, t) issuerKP := createAccountNKey(t) issuerPK := publicKey(issuerKP, t) account := NewAccountClaims(issuerAccountPK) account.SigningKeys.Add(issuerPK) token, err := account.Encode(issuerAccountKP) if err != nil { t.Fatal(err) } account, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } importerKP := createAccountNKey(t) importerPK := publicKey(importerKP, t) ac := NewActivationClaims(importerPK) ac.IssuerAccount = issuerAccountPK ac.Name = "foo.bar" ac.Activation.ImportSubject = Subject("foo.bar") ac.Activation.ImportType = Stream var vr ValidationResults ac.Validate(&vr) if len(vr.Issues) != 0 { t.Fatalf("expected no validation errors: %v", vr.Issues[0].Error()) } token, err = ac.Encode(issuerKP) if err != nil { t.Fatal(err) } ac, err = DecodeActivationClaims(token) if err != nil { t.Fatal(err) } if ac.Issuer != issuerPK { t.Fatal("expected activation subject to be different") } if ac.IssuerAccount != issuerAccountPK { t.Fatal("expected activation account id to be different") } if !account.DidSign(ac) { t.Fatal("expected account to have signed activation") } ac.IssuerAccount = publicKey(createUserNKey(t), t) ac.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected validation error") } } func TestCleanSubject(t *testing.T) { input := [][]string{ {"foo", "foo"}, {"*", "_"}, {">", "_"}, {"foo.*", "foo"}, {"foo.bar.>", "foo.bar"}, {"foo.*.bar", "foo"}, {"bam.boom.blat.*", "bam.boom.blat"}, {"*.blam", "_"}, } for _, pair := range input { clean := cleanSubject(pair[0]) if pair[1] != clean { t.Errorf("Expected %s but got %s", pair[1], clean) } } } func TestActivationClaimRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) e := &Export{Subject: "q.>", Type: Service, TokenReq: true} account.Exports.Add(e) a := publicKey(createAccountNKey(t), t) aminAgo := time.Now().Add(-time.Minute) if account.Exports[0].Revocations.IsRevoked(a, aminAgo) { t.Fatal("should not be revoked") } e.RevokeAt(a, aminAgo) if !account.Exports[0].Revocations.IsRevoked(a, aminAgo) { t.Fatal("should be revoked") } a2 := publicKey(createAccountNKey(t), t) if account.Exports[0].Revocations.IsRevoked(a2, aminAgo) { t.Fatal("should not be revoked") } e.RevokeAt("*", aminAgo) if !account.Exports[0].Revocations.IsRevoked(a2, time.Now().Add(-time.Hour)) { t.Fatal("should be revoked") } vr := ValidationResults{} account.Validate(&vr) if !vr.IsEmpty() { t.Fatal("account validation shouldn't have failed") } } jwt-2.7.3/v2/v1compat/claims.go000066400000000000000000000165671472706253100162350ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "crypto/sha512" "encoding/base32" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "time" "github.com/nats-io/nkeys" ) // ClaimType is used to indicate the type of JWT being stored in a Claim type ClaimType string const ( // AccountClaim is the type of an Account JWT AccountClaim = "account" //ActivationClaim is the type of an activation JWT ActivationClaim = "activation" //UserClaim is the type of an user JWT UserClaim = "user" //OperatorClaim is the type of an operator JWT OperatorClaim = "operator" //ServerClaim is the type of an server JWT // Deprecated: ServerClaim is not supported ServerClaim = "server" // ClusterClaim is the type of an cluster JWT // Deprecated: ClusterClaim is not supported ClusterClaim = "cluster" ) // Claims is a JWT claims type Claims interface { Claims() *ClaimsData Encode(kp nkeys.KeyPair) (string, error) ExpectedPrefixes() []nkeys.PrefixByte Payload() interface{} String() string Validate(vr *ValidationResults) Verify(payload string, sig []byte) bool } // ClaimsData is the base struct for all claims type ClaimsData struct { Audience string `json:"aud,omitempty"` Expires int64 `json:"exp,omitempty"` ID string `json:"jti,omitempty"` IssuedAt int64 `json:"iat,omitempty"` Issuer string `json:"iss,omitempty"` Name string `json:"name,omitempty"` NotBefore int64 `json:"nbf,omitempty"` Subject string `json:"sub,omitempty"` Tags TagList `json:"tags,omitempty"` Type ClaimType `json:"type,omitempty"` } // Prefix holds the prefix byte for an NKey type Prefix struct { nkeys.PrefixByte } func encodeToString(d []byte) string { return base64.RawURLEncoding.EncodeToString(d) } func decodeString(s string) ([]byte, error) { return base64.RawURLEncoding.DecodeString(s) } func serialize(v interface{}) (string, error) { j, err := json.Marshal(v) if err != nil { return "", err } return encodeToString(j), nil } func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims) (string, error) { if header == nil { return "", errors.New("header is required") } if kp == nil { return "", errors.New("keypair is required") } if c.Subject == "" { return "", errors.New("subject is not set") } h, err := serialize(header) if err != nil { return "", err } issuerBytes, err := kp.PublicKey() if err != nil { return "", err } prefixes := claim.ExpectedPrefixes() if prefixes != nil { ok := false for _, p := range prefixes { switch p { case nkeys.PrefixByteAccount: if nkeys.IsValidPublicAccountKey(issuerBytes) { ok = true } case nkeys.PrefixByteOperator: if nkeys.IsValidPublicOperatorKey(issuerBytes) { ok = true } case nkeys.PrefixByteServer: if nkeys.IsValidPublicServerKey(issuerBytes) { ok = true } case nkeys.PrefixByteCluster: if nkeys.IsValidPublicClusterKey(issuerBytes) { ok = true } case nkeys.PrefixByteUser: if nkeys.IsValidPublicUserKey(issuerBytes) { ok = true } } } if !ok { return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes) } } c.Issuer = string(issuerBytes) c.IssuedAt = time.Now().UTC().Unix() c.ID, err = c.hash() if err != nil { return "", err } payload, err := serialize(claim) if err != nil { return "", err } sig, err := kp.Sign([]byte(payload)) if err != nil { return "", err } eSig := encodeToString(sig) return fmt.Sprintf("%s.%s.%s", h, payload, eSig), nil } func (c *ClaimsData) hash() (string, error) { j, err := json.Marshal(c) if err != nil { return "", err } h := sha512.New512_256() h.Write(j) return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil } // Encode encodes a claim into a JWT token. The claim is signed with the // provided nkey's private key func (c *ClaimsData) Encode(kp nkeys.KeyPair, payload Claims) (string, error) { return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload) } // Returns a JSON representation of the claim func (c *ClaimsData) String(claim interface{}) string { j, err := json.MarshalIndent(claim, "", " ") if err != nil { return "" } return string(j) } func parseClaims(s string, target Claims) error { h, err := decodeString(s) if err != nil { return err } return json.Unmarshal(h, &target) } // Verify verifies that the encoded payload was signed by the // provided public key. Verify is called automatically with // the claims portion of the token and the public key in the claim. // Client code need to insure that the public key in the // claim is trusted. func (c *ClaimsData) Verify(payload string, sig []byte) bool { // decode the public key kp, err := nkeys.FromPublicKey(c.Issuer) if err != nil { return false } if err := kp.Verify([]byte(payload), sig); err != nil { return false } return true } // Validate checks a claim to make sure it is valid. Validity checks // include expiration and not before constraints. func (c *ClaimsData) Validate(vr *ValidationResults) { now := time.Now().UTC().Unix() if c.Expires > 0 && now > c.Expires { vr.AddTimeCheck("claim is expired") } if c.NotBefore > 0 && c.NotBefore > now { vr.AddTimeCheck("claim is not yet valid") } } // IsSelfSigned returns true if the claims issuer is the subject func (c *ClaimsData) IsSelfSigned() bool { return c.Issuer == c.Subject } // Decode takes a JWT string decodes it and validates it // and return the embedded Claims. If the token header // doesn't match the expected algorithm, or the claim is // not valid or verification fails an error is returned. func Decode(token string, target Claims) error { // must have 3 chunks chunks := strings.Split(token, ".") if len(chunks) != 3 { return errors.New("expected 3 chunks") } _, err := parseHeaders(chunks[0]) if err != nil { return err } if err := parseClaims(chunks[1], target); err != nil { return err } sig, err := decodeString(chunks[2]) if err != nil { return err } if !target.Verify(chunks[1], sig) { return errors.New("claim failed signature verification") } prefixes := target.ExpectedPrefixes() if prefixes != nil { ok := false issuer := target.Claims().Issuer for _, p := range prefixes { switch p { case nkeys.PrefixByteAccount: if nkeys.IsValidPublicAccountKey(issuer) { ok = true } case nkeys.PrefixByteOperator: if nkeys.IsValidPublicOperatorKey(issuer) { ok = true } case nkeys.PrefixByteServer: if nkeys.IsValidPublicServerKey(issuer) { ok = true } case nkeys.PrefixByteCluster: if nkeys.IsValidPublicClusterKey(issuer) { ok = true } case nkeys.PrefixByteUser: if nkeys.IsValidPublicUserKey(issuer) { ok = true } } } if !ok { return fmt.Errorf("unable to validate expected prefixes - %v", prefixes) } } return nil } jwt-2.7.3/v2/v1compat/cluster_claims.go000066400000000000000000000055551472706253100177710ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt import ( "errors" "github.com/nats-io/nkeys" ) // Cluster stores the cluster specific elements of a cluster JWT // Deprecated: ClusterClaims are not supported type Cluster struct { Trust []string `json:"identity,omitempty"` Accounts []string `json:"accts,omitempty"` AccountURL string `json:"accturl,omitempty"` OperatorURL string `json:"opurl,omitempty"` } // Validate checks the cluster and permissions for a cluster JWT func (c *Cluster) Validate(vr *ValidationResults) { // fixme validate cluster data } // ClusterClaims defines the data in a cluster JWT // Deprecated: ClusterClaims are not supported type ClusterClaims struct { ClaimsData Cluster `json:"nats,omitempty"` } // NewClusterClaims creates a new cluster JWT with the specified subject/public key // Deprecated: ClusterClaims are not supported func NewClusterClaims(subject string) *ClusterClaims { if subject == "" { return nil } c := &ClusterClaims{} c.Subject = subject return c } // Encode tries to turn the cluster claims into a JWT string func (c *ClusterClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicClusterKey(c.Subject) { return "", errors.New("expected subject to be a cluster public key") } c.ClaimsData.Type = ClusterClaim return c.ClaimsData.Encode(pair, c) } // DecodeClusterClaims tries to parse cluster claims from a JWT string // Deprecated: ClusterClaims are not supported func DecodeClusterClaims(token string) (*ClusterClaims, error) { v := ClusterClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } func (c *ClusterClaims) String() string { return c.ClaimsData.String(c) } // Payload returns the cluster specific data func (c *ClusterClaims) Payload() interface{} { return &c.Cluster } // Validate checks the generic and cluster data in the cluster claims func (c *ClusterClaims) Validate(vr *ValidationResults) { c.ClaimsData.Validate(vr) c.Cluster.Validate(vr) } // ExpectedPrefixes defines the types that can encode a cluster JWT, operator or cluster func (c *ClusterClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteOperator, nkeys.PrefixByteCluster} } // Claims returns the generic data func (c *ClusterClaims) Claims() *ClaimsData { return &c.ClaimsData } jwt-2.7.3/v2/v1compat/cluster_claims_test.go000066400000000000000000000062411472706253100210210ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewClusterClaims(t *testing.T) { ckp := createClusterNKey(t) skp := createClusterNKey(t) uc := NewClusterClaims(publicKey(skp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, ckp, t) uc2, err := DecodeClusterClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestClusterClaimsIssuer(t *testing.T) { ckp := createClusterNKey(t) skp := createClusterNKey(t) uc := NewClusterClaims(publicKey(skp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, ckp, t) temp, err := DecodeGeneric(uJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), true}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeClusterClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode cluster signed by %q", i.name) t.Fail() } } } func TestClusterSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"server", createServerNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"cluster", createClusterNKey(t), true}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewClusterClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode cluster with with %q subject", i.name) t.Fail() } } } func TestNewNilClusterClaims(t *testing.T) { v := NewClusterClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestClusterType(t *testing.T) { c := NewClusterClaims(publicKey(createClusterNKey(t), t)) s := encode(c, createClusterNKey(t), t) u, err := DecodeClusterClaims(s) if err != nil { t.Fatalf("failed to decode cluster claim: %v", err) } if ClusterClaim != u.Type { t.Fatalf("type is unexpected %q (wanted cluster)", u.Type) } } jwt-2.7.3/v2/v1compat/creds_utils.go000066400000000000000000000134011472706253100172650ustar00rootroot00000000000000/* * Copyright 2019-2020 The NATS Authors * 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. */ package jwt import ( "bytes" "errors" "fmt" "regexp" "strings" "github.com/nats-io/nkeys" ) // DecorateJWT returns a decorated JWT that describes the kind of JWT func DecorateJWT(jwtString string) ([]byte, error) { gc, err := DecodeGeneric(jwtString) if err != nil { return nil, err } return formatJwt(string(gc.Type), jwtString) } func formatJwt(kind string, jwtString string) ([]byte, error) { templ := `-----BEGIN NATS %s JWT----- %s ------END NATS %s JWT------ ` w := bytes.NewBuffer(nil) kind = strings.ToUpper(kind) _, err := fmt.Fprintf(w, templ, kind, jwtString, kind) if err != nil { return nil, err } return w.Bytes(), nil } // DecorateSeed takes a seed and returns a string that wraps // the seed in the form: // // ************************* IMPORTANT ************************* // NKEY Seed printed below can be used sign and prove identity. // NKEYs are sensitive and should be treated as secrets. // // -----BEGIN USER NKEY SEED----- // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM // ------END USER NKEY SEED------ func DecorateSeed(seed []byte) ([]byte, error) { w := bytes.NewBuffer(nil) ts := bytes.TrimSpace(seed) pre := string(ts[0:2]) kind := "" switch pre { case "SU": kind = "USER" case "SA": kind = "ACCOUNT" case "SO": kind = "OPERATOR" default: return nil, errors.New("seed is not an operator, account or user seed") } header := `************************* IMPORTANT ************************* NKEY Seed printed below can be used to sign and prove identity. NKEYs are sensitive and should be treated as secrets. -----BEGIN %s NKEY SEED----- ` _, err := fmt.Fprintf(w, header, kind) if err != nil { return nil, err } w.Write(ts) footer := ` ------END %s NKEY SEED------ ************************************************************* ` _, err = fmt.Fprintf(w, footer, kind) if err != nil { return nil, err } return w.Bytes(), nil } var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`) // An user config file looks like this: // -----BEGIN NATS USER JWT----- // eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5... // ------END NATS USER JWT------ // // ************************* IMPORTANT ************************* // NKEY Seed printed below can be used sign and prove identity. // NKEYs are sensitive and should be treated as secrets. // // -----BEGIN USER NKEY SEED----- // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM // ------END USER NKEY SEED------ // FormatUserConfig returns a decorated file with a decorated JWT and decorated seed func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) { gc, err := DecodeGeneric(jwtString) if err != nil { return nil, err } if gc.Type != UserClaim { return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.Type)) } w := bytes.NewBuffer(nil) jd, err := formatJwt(string(gc.Type), jwtString) if err != nil { return nil, err } _, err = w.Write(jd) if err != nil { return nil, err } if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) { return nil, fmt.Errorf("nkey seed is not an user seed") } d, err := DecorateSeed(seed) if err != nil { return nil, err } _, err = w.Write(d) if err != nil { return nil, err } return w.Bytes(), nil } // ParseDecoratedJWT takes a creds file and returns the JWT portion. func ParseDecoratedJWT(contents []byte) (string, error) { items := userConfigRE.FindAllSubmatch(contents, -1) if len(items) == 0 { return string(contents), nil } // First result should be the user JWT. // We copy here so that if the file contained a seed file too we wipe appropriately. raw := items[0][1] tmp := make([]byte, len(raw)) copy(tmp, raw) return string(tmp), nil } // ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a // key pair from it. func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) { var seed []byte items := userConfigRE.FindAllSubmatch(contents, -1) if len(items) > 1 { seed = items[1][1] } else { lines := bytes.Split(contents, []byte("\n")) for _, line := range lines { if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) || bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) || bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) { seed = line break } } } if seed == nil { return nil, errors.New("no nkey seed found") } if !bytes.HasPrefix(seed, []byte("SO")) && !bytes.HasPrefix(seed, []byte("SA")) && !bytes.HasPrefix(seed, []byte("SU")) { return nil, errors.New("doesn't contain a seed nkey") } kp, err := nkeys.FromSeed(seed) if err != nil { return nil, err } return kp, nil } // ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a // key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys. func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) { nk, err := ParseDecoratedNKey(contents) if err != nil { return nil, err } seed, err := nk.Seed() if err != nil { return nil, err } if !bytes.HasPrefix(seed, []byte("SU")) { return nil, errors.New("doesn't contain an user seed nkey") } kp, err := nkeys.FromSeed(seed) if err != nil { return nil, err } return kp, nil } jwt-2.7.3/v2/v1compat/creds_utils_test.go000066400000000000000000000117321472706253100203310ustar00rootroot00000000000000/* * Copyright 2019-2020 The NATS Authors * 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. */ package jwt import ( "bytes" "fmt" "strings" "testing" "github.com/nats-io/nkeys" ) func makeJWT(t *testing.T) (string, nkeys.KeyPair) { akp := createAccountNKey(t) kp := createUserNKey(t) pk := publicKey(kp, t) oc := NewUserClaims(pk) token, err := oc.Encode(akp) if err != nil { t.Fatal(err) } return token, kp } func Test_DecorateJwt(t *testing.T) { token, _ := makeJWT(t) d, err := DecorateJWT(token) if err != nil { t.Fatal(err) } s := string(d) if !strings.Contains(s, "-BEGIN NATS USER JWT-") { t.Fatal("doesn't contain expected header") } if !strings.Contains(s, "eyJ0") { t.Fatal("doesn't contain public key") } if !strings.Contains(s, "-END NATS USER JWT------\n\n") { t.Fatal("doesn't contain expected footer") } } func Test_FormatUserConfig(t *testing.T) { token, kp := makeJWT(t) d, err := FormatUserConfig(token, seedKey(kp, t)) if err != nil { t.Fatal(err) } s := string(d) if !strings.Contains(s, "-BEGIN NATS USER JWT-") { t.Fatal("doesn't contain expected header") } if !strings.Contains(s, "eyJ0") { t.Fatal("doesn't contain public key") } if !strings.Contains(s, "-END NATS USER JWT-") { t.Fatal("doesn't contain expected footer") } validateSeed(t, d, kp) } func validateSeed(t *testing.T, decorated []byte, nk nkeys.KeyPair) { kind := "" seed := seedKey(nk, t) switch string(seed[0:2]) { case "SO": kind = "operator" case "SA": kind = "account" case "SU": kind = "user" default: kind = "not supported" } kind = strings.ToUpper(kind) s := string(decorated) if !strings.Contains(s, fmt.Sprintf("\n\n-----BEGIN %s NKEY SEED-", kind)) { t.Fatal("doesn't contain expected seed header") } if !strings.Contains(s, string(seed)) { t.Fatal("doesn't contain the seed") } if !strings.Contains(s, fmt.Sprintf("-END %s NKEY SEED------\n\n", kind)) { t.Fatal("doesn't contain expected seed footer") } } func Test_ParseDecoratedJWT(t *testing.T) { token, _ := makeJWT(t) t2, err := ParseDecoratedJWT([]byte(token)) if err != nil { t.Fatal(err) } if token != t2 { t.Fatal("jwt didn't match expected") } decorated, err := DecorateJWT(token) if err != nil { t.Fatal(err) } t3, err := ParseDecoratedJWT(decorated) if err != nil { t.Fatal(err) } if token != t3 { t.Fatal("parse decorated jwt didn't match expected") } } func Test_ParseDecoratedJWTBad(t *testing.T) { v, err := ParseDecoratedJWT([]byte("foo")) if err != nil { t.Fatal(err) } if v != "foo" { t.Fatal("unexpected input was not returned") } } func Test_ParseDecoratedSeed(t *testing.T) { token, ukp := makeJWT(t) us := seedKey(ukp, t) decorated, err := FormatUserConfig(token, us) if err != nil { t.Fatal(err) } kp, err := ParseDecoratedUserNKey(decorated) if err != nil { t.Fatal(err) } pu := seedKey(kp, t) if !bytes.Equal(us, pu) { t.Fatal("seeds don't match") } } func Test_ParseDecoratedBadKey(t *testing.T) { token, ukp := makeJWT(t) us, err := ukp.Seed() if err != nil { t.Fatal(err) } akp := createAccountNKey(t) as := seedKey(akp, t) _, err = FormatUserConfig(token, as) if err == nil { t.Fatal("should have failed to encode with bad seed") } sc, err := FormatUserConfig(token, us) if err != nil { t.Fatal(err) } bad := strings.Replace(string(sc), string(us), string(as), -1) _, err = ParseDecoratedUserNKey([]byte(bad)) if err == nil { t.Fatal("parse should have failed for non user nkey") } } func Test_FailsOnNonUserJWT(t *testing.T) { akp := createAccountNKey(t) pk := publicKey(akp, t) ac := NewAccountClaims(pk) token, err := ac.Encode(akp) if err != nil { t.Fatal(err) } ukp := createUserNKey(t) us := seedKey(ukp, t) _, err = FormatUserConfig(token, us) if err == nil { t.Fatal("should have failed with account claims") } } func Test_DecorateNKeys(t *testing.T) { var kps []nkeys.KeyPair kps = append(kps, createOperatorNKey(t)) kps = append(kps, createAccountNKey(t)) kps = append(kps, createUserNKey(t)) for _, kp := range kps { seed := seedKey(kp, t) d, err := DecorateSeed(seed) if err != nil { t.Fatal(err, string(seed)) } validateSeed(t, d, kp) kp2, err := ParseDecoratedNKey(d) if err != nil { t.Fatal(string(seed), err) } seed2 := seedKey(kp2, t) if !bytes.Equal(seed, seed2) { t.Fatalf("seeds dont match %q != %q", string(seed), string(seed2)) } } _, err := ParseDecoratedNKey([]byte("bad")) if err == nil { t.Fatal("required error parsing bad nkey") } } jwt-2.7.3/v2/v1compat/decoder_test.go000066400000000000000000000232101472706253100174100ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "reflect" "strings" "testing" "time" "github.com/nats-io/nkeys" ) func TestNewToken(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } claims := NewGenericClaims(publicKey(createUserNKey(t), t)) claims.Data["foo"] = "bar" token, err := claims.Encode(kp) if err != nil { t.Fatal("error encoding token", err) } c, err := DecodeGeneric(token) if err != nil { t.Fatal(err) } if claims.NotBefore != c.NotBefore { t.Fatal("notbefore don't match") } if claims.Issuer != c.Issuer { t.Fatal("notbefore don't match") } if !reflect.DeepEqual(claims.Data, c.Data) { t.Fatal("data sections don't match") } } func TestBadType(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{"JWS", AlgorithmNkey} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c) if err != nil { t.Fatal(err) } claim, err := DecodeGeneric(token) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != fmt.Sprintf("not supported type %q", "JWS") { t.Fatal("expected not supported type error") } } func TestBadAlgo(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{TokenTypeJwt, "foobar"} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c) if err != nil { t.Fatal(err) } claim, err := DecodeGeneric(token) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != fmt.Sprintf("unexpected %q algorithm", "foobar") { t.Fatal("expected unexpected algorithm") } } func TestBadJWT(t *testing.T) { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } h := Header{"JWS", AlgorithmNkey} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c) if err != nil { t.Fatal(err) } chunks := strings.Split(token, ".") badToken := fmt.Sprintf("%s.%s", chunks[0], chunks[1]) claim, err := DecodeGeneric(badToken) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != "expected 3 chunks" { t.Fatalf("unexpeced error: %q", err.Error()) } } func TestBadSignature(t *testing.T) { kp := createAccountNKey(t) h := Header{TokenTypeJwt, AlgorithmNkey} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" token, err := c.doEncode(&h, kp, c) if err != nil { t.Fatal(err) } token = token + "A" claim, err := DecodeGeneric(token) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != "claim failed signature verification" { m := fmt.Sprintf("expected failed signature: %q", err.Error()) t.Fatal(m) } } func TestDifferentPayload(t *testing.T) { akp1 := createAccountNKey(t) c1 := NewGenericClaims(publicKey(createUserNKey(t), t)) c1.Data["foo"] = "barz" jwt1 := encode(c1, akp1, t) c1t := strings.Split(jwt1, ".") c1.Data["foo"] = "bar" kp2 := createAccountNKey(t) token2 := encode(c1, kp2, t) c2t := strings.Split(token2, ".") c1t[1] = c2t[1] claim, err := DecodeGeneric(fmt.Sprintf("%s.%s.%s", c1t[0], c1t[1], c1t[2])) if claim != nil { t.Fatal("non nil claim on bad token") } if err == nil { t.Fatal("nil error on bad token") } if err.Error() != "claim failed signature verification" { m := fmt.Sprintf("expected failed signature: %q", err.Error()) t.Fatal(m) } } func TestExpiredToken(t *testing.T) { akp := createAccountNKey(t) c := NewGenericClaims(publicKey(akp, t)) c.Expires = time.Now().UTC().Unix() - 100 c.Data["foo"] = "barz" vr := CreateValidationResults() c.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("expired tokens should be blocking when time is included") } if vr.IsBlocking(false) { t.Fatalf("expired tokens should not be blocking when time is not included") } } func TestNotYetValid(t *testing.T) { akp1, err := nkeys.CreateAccount() if err != nil { t.Fatal("unable to create account key", err) } c := NewGenericClaims(publicKey(akp1, t)) c.NotBefore = time.Now().Add(time.Duration(1) * time.Hour).UTC().Unix() vr := CreateValidationResults() c.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("not yet valid tokens should be blocking when time is included") } if vr.IsBlocking(false) { t.Fatalf("not yet valid tokens should not be blocking when time is not included") } } func TestIssuedAtIsSet(t *testing.T) { akp := createAccountNKey(t) c := NewGenericClaims(publicKey(akp, t)) c.Data["foo"] = "barz" token, err := c.Encode(akp) if err != nil { t.Fatal(err) } claim, err := DecodeGeneric(token) if err != nil { t.Fatalf("unexpected error: %v", err) } if claim.IssuedAt == 0 { t.Fatalf("issued at is not set") } } func TestSample(t *testing.T) { // Need a private key to sign the claim akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) // add a bunch of claims claims.Data["foo"] = "bar" // serialize the claim to a JWT token token, err := claims.Encode(akp) if err != nil { t.Fatal("error encoding token", err) } // on the receiving side, decode the token c, err := DecodeGeneric(token) if err != nil { t.Fatal(err) } // if the token was decoded, it means that it // validated and it wasn't tampered. the remaining and // required test is to insure the issuer is trusted pk, err := akp.PublicKey() if err != nil { t.Fatalf("unable to read public key: %v", err) } if c.Issuer != string(pk) { t.Fatalf("the public key is not trusted") } } func TestBadHeaderEncoding(t *testing.T) { // the '=' will be illegal _, err := parseHeaders("=hello=") if err == nil { t.Fatal("should have failed it is not encoded") } } func TestBadClaimsEncoding(t *testing.T) { // the '=' will be illegal c := GenericClaims{} err := parseClaims("=hello=", &c) if err == nil { t.Fatal("should have failed it is not encoded") } } func TestBadHeaderJSON(t *testing.T) { payload := encodeToString([]byte("{foo: bar}")) _, err := parseHeaders(payload) if err == nil { t.Fatal("should have failed bad json") } } func TestBadClaimsJSON(t *testing.T) { payload := encodeToString([]byte("{foo: bar}")) c := GenericClaims{} err := parseClaims(payload, &c) if err == nil { t.Fatal("should have failed bad json") } } func TestBadPublicKeyDecodeGeneric(t *testing.T) { c := &GenericClaims{} c.Issuer = "foo" if ok := c.Verify("foo", []byte("bar")); ok { t.Fatal("Should have failed to verify") } } func TestBadSig(t *testing.T) { opk := createOperatorNKey(t) kp := createAccountNKey(t) claims := NewGenericClaims(publicKey(kp, t)) claims.Data["foo"] = "bar" // serialize the claim to a JWT token token := encode(claims, opk, t) tokens := strings.Split(token, ".") badToken := fmt.Sprintf("%s.%s.=hello=", tokens[0], tokens[1]) _, err := DecodeGeneric(badToken) if err == nil { t.Fatal("should have failed to base64 decode signature") } } func TestClaimsStringIsJSON(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) // add a bunch of claims claims.Data["foo"] = "bar" claims2 := NewGenericClaims(publicKey(akp, t)) json.Unmarshal([]byte(claims.String()), claims2) if claims2.Data["foo"] != "bar" { t.Fatalf("Failed to decode expected claim from String representation: %q", claims.String()) } } func TestDoEncodeNilHeader(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) _, err := claims.doEncode(nil, nil, claims) if err == nil { t.Fatal("should have failed to encode") } if err.Error() != "header is required" { t.Fatalf("unexpected error on encode: %v", err) } } func TestDoEncodeNilKeyPair(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) _, err := claims.doEncode(&Header{}, nil, claims) if err == nil { t.Fatal("should have failed to encode") } if err.Error() != "keypair is required" { t.Fatalf("unexpected error on encode: %v", err) } } // if this fails, the URL decoder was changed and JWTs will flap func TestUsingURLDecoder(t *testing.T) { token := "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJGQ1lZRjJLR0EzQTZHTlZQR0pIVjNUSExYR1VZWkFUREZLV1JTT1czUUo1T0k3QlJST0ZRIiwiaWF0IjoxNTQzOTQzNjc1LCJpc3MiOiJBQ1NKWkhOWlI0QUFUVE1KNzdUV1JONUJHVUZFWFhUS0gzWEtGTldDRkFCVzJRWldOUTRDQkhRRSIsInN1YiI6IkFEVEFHWVZYRkpPRENRM0g0VUZQQU43R1dXWk1BVU9FTTJMMkRWQkFWVFdLM01TU0xUS1JUTzVGIiwidHlwZSI6ImFjdGl2YXRpb24iLCJuYXRzIjp7InN1YmplY3QiOiJmb28iLCJ0eXBlIjoic2VydmljZSJ9fQ.HCZTCF-7wolS3Wjx3swQWMkoDhoo_4gp9EsuM5diJfZrH8s6NTpO0iT7_fKZm7dNDeEoqjwU--3ebp8j-Mm_Aw" ac, err := DecodeActivationClaims(token) if err != nil { t.Fatal("shouldn't have failed to decode", err) } if ac == nil { t.Fatal("should have returned activation") } } jwt-2.7.3/v2/v1compat/exports.go000066400000000000000000000160241472706253100164550ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "fmt" "time" ) // ResponseType is used to store an export response type type ResponseType string const ( // ResponseTypeSingleton is used for a service that sends a single response only ResponseTypeSingleton = "Singleton" // ResponseTypeStream is used for a service that will send multiple responses ResponseTypeStream = "Stream" // ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream) ResponseTypeChunked = "Chunked" ) // ServiceLatency is used when observing and exported service for // latency measurements. // Sampling 1-100, represents sampling rate, defaults to 100. // Results is the subject where the latency metrics are published. // A metric will be defined by the nats-server's ServiceLatency. Time durations // are in nanoseconds. // see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524 // e.g. // // { // "app": "dlc22", // "start": "2019-09-16T21:46:23.636869585-07:00", // "svc": 219732, // "nats": { // "req": 320415, // "resp": 228268, // "sys": 0 // }, // "total": 768415 // } type ServiceLatency struct { Sampling int `json:"sampling,omitempty"` Results Subject `json:"results"` } func (sl *ServiceLatency) Validate(vr *ValidationResults) { if sl.Sampling < 1 || sl.Sampling > 100 { vr.AddError("sampling percentage needs to be between 1-100") } sl.Results.Validate(vr) if sl.Results.HasWildCards() { vr.AddError("results subject can not contain wildcards") } } // Export represents a single export type Export struct { Name string `json:"name,omitempty"` Subject Subject `json:"subject,omitempty"` Type ExportType `json:"type,omitempty"` TokenReq bool `json:"token_req,omitempty"` Revocations RevocationList `json:"revocations,omitempty"` ResponseType ResponseType `json:"response_type,omitempty"` Latency *ServiceLatency `json:"service_latency,omitempty"` AccountTokenPosition uint `json:"account_token_position,omitempty"` } // IsService returns true if an export is for a service func (e *Export) IsService() bool { return e.Type == Service } // IsStream returns true if an export is for a stream func (e *Export) IsStream() bool { return e.Type == Stream } // IsSingleResponse returns true if an export has a single response // or no resopnse type is set, also checks that the type is service func (e *Export) IsSingleResponse() bool { return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "") } // IsChunkedResponse returns true if an export has a chunked response func (e *Export) IsChunkedResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeChunked } // IsStreamResponse returns true if an export has a chunked response func (e *Export) IsStreamResponse() bool { return e.Type == Service && e.ResponseType == ResponseTypeStream } // Validate appends validation issues to the passed in results list func (e *Export) Validate(vr *ValidationResults) { if e == nil { vr.AddError("null export is not allowed") return } if !e.IsService() && !e.IsStream() { vr.AddError("invalid export type: %q", e.Type) } if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() { vr.AddError("invalid response type for service: %q", e.ResponseType) } if e.IsStream() && e.ResponseType != "" { vr.AddError("invalid response type for stream: %q", e.ResponseType) } if e.Latency != nil { if !e.IsService() { vr.AddError("latency tracking only permitted for services") } e.Latency.Validate(vr) } e.Subject.Validate(vr) } // Revoke enters a revocation by publickey using time.Now(). func (e *Export) Revoke(pubKey string) { e.RevokeAt(pubKey, time.Now()) } // RevokeAt enters a revocation by publickey and timestamp into this export // If there is already a revocation for this public key that is newer, it is kept. func (e *Export) RevokeAt(pubKey string, timestamp time.Time) { if e.Revocations == nil { e.Revocations = RevocationList{} } e.Revocations.Revoke(pubKey, timestamp) } // ClearRevocation removes any revocation for the public key func (e *Export) ClearRevocation(pubKey string) { e.Revocations.ClearRevocation(pubKey) } // IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in. // Generally this method is called with the subject and issue time of the jwt to be tested. // DO NOT pass time.Now(), it will not produce a stable/expected response. func (e *Export) IsRevokedAt(pubKey string, timestamp time.Time) bool { return e.Revocations.IsRevoked(pubKey, timestamp) } // IsRevoked does not perform a valid check. Use IsRevokedAt instead. func (e *Export) IsRevoked(_ string) bool { return true } // Exports is a slice of exports type Exports []*Export // Add appends exports to the list func (e *Exports) Add(i ...*Export) { *e = append(*e, i...) } func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { m := make(map[string]string) for i, ns := range subjects { for j, s := range subjects { if i == j { continue } if ns.IsContainedIn(s) { str := string(s) _, ok := m[str] if !ok { m[str] = string(ns) } } } } if len(m) != 0 { for k, v := range m { var vi ValidationIssue vi.Blocking = true vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v) vr.Add(&vi) } } } // Validate calls validate on all of the exports func (e *Exports) Validate(vr *ValidationResults) error { var serviceSubjects []Subject var streamSubjects []Subject for _, v := range *e { if v == nil { vr.AddError("null export is not allowed") continue } if v.IsService() { serviceSubjects = append(serviceSubjects, v.Subject) } else { streamSubjects = append(streamSubjects, v.Subject) } v.Validate(vr) } isContainedIn(Service, serviceSubjects, vr) isContainedIn(Stream, streamSubjects, vr) return nil } // HasExportContainingSubject checks if the export list has an export with the provided subject func (e *Exports) HasExportContainingSubject(subject Subject) bool { for _, s := range *e { if subject.IsContainedIn(s.Subject) { return true } } return false } func (e Exports) Len() int { return len(e) } func (e Exports) Swap(i, j int) { e[i], e[j] = e[j], e[i] } func (e Exports) Less(i, j int) bool { return e[i].Subject < e[j].Subject } jwt-2.7.3/v2/v1compat/exports_test.go000066400000000000000000000164741472706253100175250ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "sort" "testing" "time" ) func TestSimpleExportValidation(t *testing.T) { e := &Export{Subject: "foo", Type: Stream} vr := CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("simple export should validate cleanly") } e.Type = Service vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("simple export should validate cleanly") } } func TestResponseTypeValidation(t *testing.T) { e := &Export{Subject: "foo", Type: Stream, ResponseType: ResponseTypeSingleton} vr := CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("response type on stream should have an validation issue") } if e.IsSingleResponse() { t.Errorf("response type should always fail for stream") } e.Type = Service vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be single") } e.ResponseType = ResponseTypeChunked vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if e.IsSingleResponse() || !e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be chunk") } e.ResponseType = ResponseTypeStream vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if e.IsSingleResponse() || e.IsChunkedResponse() || !e.IsStreamResponse() { t.Errorf("response type should be stream") } e.ResponseType = "" vr = CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("response type on service should validate cleanly") } if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be single") } e.ResponseType = "bad" vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("response type should match available options") } if e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { t.Errorf("response type should be bad") } } func TestInvalidExportType(t *testing.T) { i := &Export{Subject: "foo", Type: Unknown} vr := CreateValidationResults() i.Validate(vr) if vr.IsEmpty() { t.Errorf("export with bad type should not validate cleanly") } if !vr.IsBlocking(true) { t.Errorf("invalid type is blocking") } } func TestOverlappingExports(t *testing.T) { i := &Export{Subject: "bar.foo", Type: Stream} i2 := &Export{Subject: "bar.*", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 1 { t.Errorf("export has overlapping subjects") } } func TestDifferentExportTypes_OverlapOK(t *testing.T) { i := &Export{Subject: "bar.foo", Type: Service} i2 := &Export{Subject: "bar.*", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 0 { t.Errorf("should allow overlaps on different export kind") } } func TestDifferentExportTypes_SameSubjectOK(t *testing.T) { i := &Export{Subject: "bar", Type: Service} i2 := &Export{Subject: "bar", Type: Stream} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 0 { t.Errorf("should allow overlaps on different export kind") } } func TestSameExportType_SameSubject(t *testing.T) { i := &Export{Subject: "bar", Type: Service} i2 := &Export{Subject: "bar", Type: Service} exports := &Exports{} exports.Add(i, i2) vr := CreateValidationResults() exports.Validate(vr) if len(vr.Issues) != 1 { t.Errorf("should not allow same subject on same export kind") } } func TestExportRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) e := &Export{Subject: "foo", Type: Stream} account.Exports.Add(e) pubKey := "bar" now := time.Now() // test that clear is safe before we add any e.ClearRevocation(pubKey) if e.IsRevokedAt(pubKey, now) { t.Errorf("no revocation was added so is revoked should be false") } e.RevokeAt(pubKey, now.Add(time.Second*100)) if !e.IsRevokedAt(pubKey, now) { t.Errorf("revocation should hold when timestamp is in the future") } if e.IsRevokedAt(pubKey, now.Add(time.Second*150)) { t.Errorf("revocation should time out") } e.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in if !e.IsRevokedAt(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should hold, 100 > 50") } encoded, _ := account.Encode(akp) decoded, _ := DecodeAccountClaims(encoded) if !decoded.Exports[0].IsRevokedAt(pubKey, now.Add(time.Second*60)) { t.Errorf("revocation should last across encoding") } e.ClearRevocation(pubKey) if e.IsRevokedAt(pubKey, now) { t.Errorf("revocations should be cleared") } e.RevokeAt(pubKey, now.Add(time.Second*1000)) if !e.IsRevoked(pubKey) { t.Errorf("revocation be true we revoked in the future") } } func TestExportTrackLatency(t *testing.T) { e := &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} vr := CreateValidationResults() e.Validate(vr) if !vr.IsEmpty() { t.Errorf("Expected to validate with simple tracking") } e = &Export{Subject: "foo", Type: Stream} e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("adding latency tracking to a stream should have an validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 0, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Sampling <1 should have a validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 122, Results: "results"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Sampling >100 should have a validation issue") } e = &Export{Subject: "foo", Type: Service} e.Latency = &ServiceLatency{Sampling: 22, Results: "results.*"} vr = CreateValidationResults() e.Validate(vr) if vr.IsEmpty() { t.Errorf("Results subject needs to be valid publish subject") } } func TestExport_Sorting(t *testing.T) { var exports Exports exports.Add(&Export{Subject: "x", Type: Service}) exports.Add(&Export{Subject: "z", Type: Service}) exports.Add(&Export{Subject: "y", Type: Service}) if exports[0].Subject != "x" { t.Fatal("added export not in expected order") } sort.Sort(exports) if exports[0].Subject != "x" && exports[1].Subject != "y" && exports[2].Subject != "z" { t.Fatal("exports not sorted") } } jwt-2.7.3/v2/v1compat/genericclaims_test.go000066400000000000000000000032571472706253100206210ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "testing" "time" ) func TestNewGenericClaims(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) uc := NewGenericClaims(apk) uc.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() uc.Name = "alberto" uc.Audience = "everyone" uc.NotBefore = time.Now().UTC().Unix() uc.Tags.Add("one") uc.Tags.Add("one") uc.Tags.Add("one") uc.Tags.Add("TWO") // should become lower case uc.Tags.Add("three") uJwt := encode(uc, akp, t) uc2, err := DecodeGeneric(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Name, uc2.Name, t) AssertEquals(uc.Audience, uc2.Audience, t) AssertEquals(uc.Expires, uc2.Expires, t) AssertEquals(uc.NotBefore, uc2.NotBefore, t) AssertEquals(uc.Subject, uc2.Subject, t) AssertEquals(3, len(uc2.Tags), t) AssertEquals(true, uc2.Tags.Contains("two"), t) AssertEquals("one", uc2.Tags[0], t) AssertEquals("two", uc2.Tags[1], t) AssertEquals("three", uc2.Tags[2], t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } jwt-2.7.3/v2/v1compat/genericlaims.go000066400000000000000000000040771472706253100174200ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import "github.com/nats-io/nkeys" // GenericClaims can be used to read a JWT as a map for any non-generic fields type GenericClaims struct { ClaimsData Data map[string]interface{} `json:"nats,omitempty"` } // NewGenericClaims creates a map-based Claims func NewGenericClaims(subject string) *GenericClaims { if subject == "" { return nil } c := GenericClaims{} c.Subject = subject c.Data = make(map[string]interface{}) return &c } // DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map func DecodeGeneric(token string) (*GenericClaims, error) { v := GenericClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } // Claims returns the standard part of the generic claim func (gc *GenericClaims) Claims() *ClaimsData { return &gc.ClaimsData } // Payload returns the custom part of the claims data func (gc *GenericClaims) Payload() interface{} { return &gc.Data } // Encode takes a generic claims and creates a JWT string func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) { return gc.ClaimsData.Encode(pair, gc) } // Validate checks the generic part of the claims data func (gc *GenericClaims) Validate(vr *ValidationResults) { gc.ClaimsData.Validate(vr) } func (gc *GenericClaims) String() string { return gc.ClaimsData.String(gc) } // ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte { return nil } jwt-2.7.3/v2/v1compat/header.go000066400000000000000000000035451472706253100162050ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "strings" ) const ( // Version is semantic version. Version = "1.2.2" // TokenTypeJwt is the JWT token type supported JWT tokens // encoded and decoded by this library TokenTypeJwt = "jwt" // AlgorithmNkey is the algorithm supported by JWT tokens // encoded and decoded by this library AlgorithmNkey = "ed25519" ) // Header is a JWT Jose Header type Header struct { Type string `json:"typ"` Algorithm string `json:"alg"` } // Parses a header JWT token func parseHeaders(s string) (*Header, error) { h, err := decodeString(s) if err != nil { return nil, err } header := Header{} if err := json.Unmarshal(h, &header); err != nil { return nil, err } if err := header.Valid(); err != nil { return nil, err } return &header, nil } // Valid validates the Header. It returns nil if the Header is // a JWT header, and the algorithm used is the NKEY algorithm. func (h *Header) Valid() error { if TokenTypeJwt != strings.ToLower(h.Type) { return fmt.Errorf("not supported type %q", h.Type) } if alg := strings.ToLower(h.Algorithm); alg != AlgorithmNkey { if alg == "ed25519-nkey" { return fmt.Errorf("more recent jwt version") } return fmt.Errorf("unexpected %q algorithm", h.Algorithm) } return nil } jwt-2.7.3/v2/v1compat/imports.go000066400000000000000000000115311472706253100164440ustar00rootroot00000000000000/* * Copyright 2018-2022 The NATS Authors * 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. */ package jwt import ( "io" "net/http" "net/url" "time" ) // Import describes a mapping from another account into this one type Import struct { Name string `json:"name,omitempty"` // 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). Subject Subject `json:"subject,omitempty"` Account string `json:"account,omitempty"` Token string `json:"token,omitempty"` // 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. To Subject `json:"to,omitempty"` Type ExportType `json:"type,omitempty"` } // IsService returns true if the import is of type service func (i *Import) IsService() bool { return i.Type == Service } // IsStream returns true if the import is of type stream func (i *Import) IsStream() bool { return i.Type == Stream } // Validate checks if an import is valid for the wrapping account func (i *Import) Validate(actPubKey string, vr *ValidationResults) { if i == nil { vr.AddError("null import is not allowed") return } if !i.IsService() && !i.IsStream() { vr.AddError("invalid import type: %q", i.Type) } if i.Account == "" { vr.AddError("account to import from is not specified") } i.Subject.Validate(vr) if i.IsService() && i.Subject.HasWildCards() { vr.AddError("services cannot have wildcard subject: %q", i.Subject) } if i.IsStream() && i.To.HasWildCards() { vr.AddError("streams cannot have wildcard to subject: %q", i.Subject) } var act *ActivationClaims if i.Token != "" { // Check to see if its an embedded JWT or a URL. if u, err := url.Parse(i.Token); err == nil && u.Scheme != "" { c := &http.Client{Timeout: 5 * time.Second} resp, err := c.Get(u.String()) if err != nil { vr.AddError("import %s contains an unreachable token URL %q", i.Subject, i.Token) } if resp != nil { defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { vr.AddError("import %s contains an unreadable token URL %q", i.Subject, i.Token) } else { act, err = DecodeActivationClaims(string(body)) if err != nil { vr.AddError("import %s contains a URL %q with an invalid activation token", i.Subject, i.Token) } } } } else { var err error act, err = DecodeActivationClaims(i.Token) if err != nil { vr.AddError("import %q contains an invalid activation token", i.Subject) } } } if act != nil { if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { vr.AddError("activation token doesn't match account for import %q", i.Subject) } if act.ClaimsData.Subject != actPubKey { vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) } if act.ImportType != i.Type { vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) } act.validateWithTimeChecks(vr, false) subj := i.Subject if i.IsService() && i.To != "" { subj = i.To } if !subj.IsContainedIn(act.ImportSubject) { vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) } } } // Imports is a list of import structs type Imports []*Import // Validate checks if an import is valid for the wrapping account func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) { toSet := make(map[Subject]bool, len(*i)) for _, v := range *i { if v == nil { vr.AddError("null import is not allowed") continue } if v.Type == Service { if _, ok := toSet[v.To]; ok { vr.AddError("Duplicate To subjects for %q", v.To) } toSet[v.To] = true } v.Validate(acctPubKey, vr) } } // Add is a simple way to add imports func (i *Imports) Add(a ...*Import) { *i = append(*i, a...) } func (i Imports) Len() int { return len(i) } func (i Imports) Swap(j, k int) { i[j], i[k] = i[k], i[j] } func (i Imports) Less(j, k int) bool { return i[j].Subject < i[k].Subject } jwt-2.7.3/v2/v1compat/imports_test.go000066400000000000000000000226631472706253100175130ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "fmt" "net/http" "net/http/httptest" "sort" "testing" "time" ) func TestImportValidation(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } vr = CreateValidationResults() i.Validate("", vr) if !vr.IsEmpty() { t.Errorf("imports should not generate an issue") } activation := NewActivationClaims(akp) activation.Expires = time.Now().Add(time.Hour).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream actJWT := encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with token should be valid") } } func TestInvalidImportType(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Unknown} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Errorf("imports without token or url should warn the caller") } if !vr.IsBlocking(true) { t.Errorf("invalid type is blocking") } } func TestInvalidImportToken(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, Token: "bad token", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsBlocking(true) { t.Errorf("bad token should be blocking") } } func TestInvalidImportURL(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, Token: "foo://bad-token-url", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if vr.IsEmpty() { t.Errorf("imports with a bad token or url should warn the caller") } if !vr.IsBlocking(true) { t.Errorf("invalid type should be blocking") } } func TestInvalidImportTokenValuesValidation(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "bar", Account: akp2, To: "test", Type: Service} vr := CreateValidationResults() i.Validate("", vr) if vr.IsBlocking(true) { t.Errorf("imports without token or url should not be blocking") } i.Type = Service vr = CreateValidationResults() i.Validate("", vr) if vr.IsBlocking(true) { t.Errorf("imports without token or url should not be blocking") } activation := NewActivationClaims(akp) activation.Max = 1024 * 1024 activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Service actJWT := encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with token should be valid") } actJWT = encode(activation, ak, t) // wrong issuer i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with wrong issuer") } activation.Subject = akp2 // wrong subject actJWT = encode(activation, ak2, t) // right issuer i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with wrong issuer") } } func TestMissingAccountInImport(t *testing.T) { i := &Import{Subject: "foo", To: "bar", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if len(vr.Issues) != 1 { t.Errorf("expected only one issue") } if !vr.IsBlocking(true) { t.Errorf("Missing Account is blocking") } } func TestServiceImportWithWildcard(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo.*", Account: akp, To: "bar", Type: Service} vr := CreateValidationResults() i.Validate("", vr) if !vr.IsBlocking(true) { t.Errorf("expected service import with a wildcard subject to be a blocking error") } } func TestStreamImportWithWildcardPrefix(t *testing.T) { i := &Import{Subject: "foo", To: "bar.>", Type: Stream} vr := CreateValidationResults() i.Validate("", vr) if len(vr.Issues) != 2 { t.Errorf("should have registered 2 issues with this import, got %d", len(vr.Issues)) } if !vr.IsBlocking(true) { t.Fatalf("expected stream import prefix with a wildcard to produce a blocking error") } } func TestImportsValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) i := &Import{Subject: "foo", Account: akp, To: "bar", Type: Stream} i2 := &Import{Subject: "foo.*", Account: akp, To: "bar", Type: Service} imports := &Imports{} imports.Add(i, i2) vr := CreateValidationResults() imports.Validate("", vr) if len(vr.Issues) != 1 { t.Errorf("warn about wildcard service") } if !vr.IsBlocking(true) { t.Errorf("expected service import with a wildcard subject to be a blocking error") } } func TestTokenURLImportValidation(t *testing.T) { ak := createAccountNKey(t) ak2 := createAccountNKey(t) akp := publicKey(ak, t) akp2 := publicKey(ak2, t) i := &Import{Subject: "test", Account: akp2, To: "bar", Type: Stream} activation := NewActivationClaims(akp) activation.Max = 1024 * 1024 activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() activation.ImportSubject = "test" activation.ImportType = Stream actJWT := encode(activation, ak2, t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(actJWT)) })) defer ts.Close() i.Token = ts.URL vr := CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { fmt.Printf("vr is %+v\n", vr) t.Errorf("imports with token url should be valid") } i.Token = "http://Bad URL" vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with bad token url should be valid") } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("bad jwt")) })) defer ts.Close() i.Token = ts.URL vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with token url pointing to bad JWT") } ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) defer ts.Close() i.Token = ts.URL vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with token url pointing to bad url") } } func TestImportSubjectValidation(t *testing.T) { ak := createAccountNKey(t) akp := publicKey(ak, t) activation := NewActivationClaims(akp) activation.Max = 1024 * 1024 activation.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() activation.ImportSubject = "one.*" activation.ImportType = Stream ak2 := createAccountNKey(t) akp2 := publicKey(ak2, t) i := &Import{Subject: "one.two", Account: akp2, To: "bar", Type: Stream} actJWT := encode(activation, ak2, t) i.Token = actJWT vr := CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Log(vr.Issues[0].Description) t.Errorf("imports with valid contains subject should be valid") } activation.ImportSubject = "two" activation.ImportType = Stream actJWT = encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if vr.IsEmpty() { t.Errorf("imports with non-contains subject should be not valid") } activation.ImportSubject = ">" activation.ImportType = Stream actJWT = encode(activation, ak2, t) i.Token = actJWT vr = CreateValidationResults() i.Validate(akp, vr) if !vr.IsEmpty() { t.Errorf("imports with valid contains subject should be valid") } } func TestImportServiceDoubleToSubjectsValidation(t *testing.T) { akp := createAccountNKey(t) akp2 := createAccountNKey(t) apk := publicKey(akp, t) apk2 := publicKey(akp2, t) account := NewAccountClaims(apk) i := &Import{Subject: "one.two", Account: apk2, To: "foo.bar", Type: Service} account.Imports.Add(i) vr := CreateValidationResults() account.Validate(vr) if vr.IsBlocking(true) { t.Fatalf("Expected no blocking validation errors") } i2 := &Import{Subject: "two.three", Account: apk2, To: "foo.bar", Type: Service} account.Imports.Add(i2) vr = CreateValidationResults() account.Validate(vr) if !vr.IsBlocking(true) { t.Fatalf("Expected multiple import 'to' subjects to produce an error") } } func TestImport_Sorting(t *testing.T) { var imports Imports pk := publicKey(createAccountNKey(t), t) imports.Add(&Import{Subject: "x", Type: Service, Account: pk}) imports.Add(&Import{Subject: "z", Type: Service, Account: pk}) imports.Add(&Import{Subject: "y", Type: Service, Account: pk}) if imports[0].Subject != "x" { t.Fatal("added import not in expected order") } sort.Sort(imports) if imports[0].Subject != "x" && imports[1].Subject != "y" && imports[2].Subject != "z" { t.Fatal("imports not sorted") } } jwt-2.7.3/v2/v1compat/operator_claims.go000066400000000000000000000137541472706253100201430ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "net/url" "strings" "github.com/nats-io/nkeys" ) // Operator specific claims type Operator struct { // Slice of real identities (like websites) that can be used to identify the operator. Identities []Identity `json:"identity,omitempty"` // Slice of other operator NKeys that can be used to sign on behalf of the main // operator identity. SigningKeys StringList `json:"signing_keys,omitempty"` // AccountServerURL is a partial URL like "https://host.domain.org:/jwt/v1" // tools will use the prefix and build queries by appending /accounts/ // 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. AccountServerURL string `json:"account_server_url,omitempty"` // A list of NATS urls (tls://host:port) where tools can connect to the server // using proper credentials. OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` // Identity of the system account SystemAccount string `json:"system_account,omitempty"` } // Validate checks the validity of the operators contents func (o *Operator) Validate(vr *ValidationResults) { if err := o.validateAccountServerURL(); err != nil { vr.AddError(err.Error()) } for _, v := range o.validateOperatorServiceURLs() { if v != nil { vr.AddError(v.Error()) } } for _, i := range o.Identities { i.Validate(vr) } for _, k := range o.SigningKeys { if !nkeys.IsValidPublicOperatorKey(k) { vr.AddError("%s is not an operator public key", k) } } if o.SystemAccount != "" { if !nkeys.IsValidPublicAccountKey(o.SystemAccount) { vr.AddError("%s is not an account public key", o.SystemAccount) } } } func (o *Operator) validateAccountServerURL() error { if o.AccountServerURL != "" { // We don't care what kind of URL it is so long as it parses // and has a protocol. The account server may impose additional // constraints on the type of URLs that it is able to notify to u, err := url.Parse(o.AccountServerURL) if err != nil { return fmt.Errorf("error parsing account server url: %v", err) } if u.Scheme == "" { return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL) } } return nil } // ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url. func ValidateOperatorServiceURL(v string) error { // should be possible for the service url to not be expressed if v == "" { return nil } u, err := url.Parse(v) if err != nil { return fmt.Errorf("error parsing operator service url %q: %v", v, err) } if u.User != nil { return fmt.Errorf("operator service url %q - credentials are not supported", v) } if u.Path != "" { return fmt.Errorf("operator service url %q - paths are not supported", v) } lcs := strings.ToLower(u.Scheme) switch lcs { case "nats": return nil case "tls": return nil default: return fmt.Errorf("operator service url %q - protocol not supported (only 'nats' or 'tls' only)", v) } } func (o *Operator) validateOperatorServiceURLs() []error { var errs []error for _, v := range o.OperatorServiceURLs { if v != "" { if err := ValidateOperatorServiceURL(v); err != nil { errs = append(errs, err) } } } return errs } // OperatorClaims define the data for an operator JWT type OperatorClaims struct { ClaimsData Operator `json:"nats,omitempty"` } // NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key func NewOperatorClaims(subject string) *OperatorClaims { if subject == "" { return nil } c := &OperatorClaims{} c.Subject = subject return c } // DidSign checks the claims against the operator's public key and its signing keys func (oc *OperatorClaims) DidSign(op Claims) bool { if op == nil { return false } issuer := op.Claims().Issuer if issuer == oc.Subject { return true } return oc.SigningKeys.Contains(issuer) } // Deprecated: AddSigningKey, use claim.SigningKeys.Add() func (oc *OperatorClaims) AddSigningKey(pk string) { oc.SigningKeys.Add(pk) } // Encode the claims into a JWT string func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicOperatorKey(oc.Subject) { return "", errors.New("expected subject to be an operator public key") } err := oc.validateAccountServerURL() if err != nil { return "", err } oc.ClaimsData.Type = OperatorClaim return oc.ClaimsData.Encode(pair, oc) } // DecodeOperatorClaims tries to create an operator claims from a JWt string func DecodeOperatorClaims(token string) (*OperatorClaims, error) { v := OperatorClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } func (oc *OperatorClaims) String() string { return oc.ClaimsData.String(oc) } // Payload returns the operator specific data for an operator JWT func (oc *OperatorClaims) Payload() interface{} { return &oc.Operator } // Validate the contents of the claims func (oc *OperatorClaims) Validate(vr *ValidationResults) { oc.ClaimsData.Validate(vr) oc.Operator.Validate(vr) } // ExpectedPrefixes defines the nkey types that can sign operator claims, operator func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteOperator} } // Claims returns the generic claims data func (oc *OperatorClaims) Claims() *ClaimsData { return &oc.ClaimsData } jwt-2.7.3/v2/v1compat/operator_claims_test.go000066400000000000000000000241661472706253100212010ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "fmt" "testing" "time" "github.com/nats-io/nkeys" ) func TestNewOperatorClaims(t *testing.T) { ckp := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, ckp, t) uc2, err := DecodeOperatorClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestOperatorSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewOperatorClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode server with with %q subject", i.name) t.Fail() } } } func TestInvalidOperatorClaimIssuer(t *testing.T) { akp := createOperatorNKey(t) ac := NewOperatorClaims(publicKey(akp, t)) ac.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() aJwt := encode(ac, akp, t) temp, err := DecodeGeneric(aJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeOperatorClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode account signed by %q", i.name) t.Fail() } } } func TestNewNilOperatorClaims(t *testing.T) { v := NewOperatorClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestOperatorType(t *testing.T) { c := NewOperatorClaims(publicKey(createOperatorNKey(t), t)) s := encode(c, createOperatorNKey(t), t) u, err := DecodeOperatorClaims(s) if err != nil { t.Fatalf("failed to decode operator claim: %v", err) } if OperatorClaim != u.Type { t.Fatalf("type is unexpected %q (wanted operator)", u.Type) } } func TestSigningKeyValidation(t *testing.T) { ckp := createOperatorNKey(t) ckp2 := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uc.AddSigningKey(publicKey(ckp2, t)) uJwt := encode(uc, ckp, t) uc2, err := DecodeOperatorClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(len(uc2.SigningKeys), 1, t) AssertEquals(uc2.SigningKeys[0] == publicKey(ckp2, t), true, t) vr := &ValidationResults{} uc.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("valid operator key should have no validation issues") } uc.AddSigningKey("") // add an invalid one vr = &ValidationResults{} uc.Validate(vr) if len(vr.Issues) != 0 { t.Fatal("should not be able to add empty values") } } func TestSignedBy(t *testing.T) { ckp := createOperatorNKey(t) ckp2 := createOperatorNKey(t) uc := NewOperatorClaims(publicKey(ckp, t)) uc2 := NewOperatorClaims(publicKey(ckp2, t)) akp := createAccountNKey(t) ac := NewAccountClaims(publicKey(akp, t)) enc, err := ac.Encode(ckp) // sign with the operator key if err != nil { t.Fatal("failed to encode", err) } ac, err = DecodeAccountClaims(enc) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.DidSign(ac), true, t) AssertEquals(uc2.DidSign(ac), false, t) enc, err = ac.Encode(ckp2) // sign with the other operator key if err != nil { t.Fatal("failed to encode", err) } ac, err = DecodeAccountClaims(enc) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.DidSign(ac), false, t) // no signing key AssertEquals(uc2.DidSign(ac), true, t) // actual key uc.AddSigningKey(publicKey(ckp2, t)) AssertEquals(uc.DidSign(ac), true, t) // signing key clusterKey := createClusterNKey(t) clusterClaims := NewClusterClaims(publicKey(clusterKey, t)) enc, err = clusterClaims.Encode(ckp2) // sign with the operator key if err != nil { t.Fatal("failed to encode", err) } clusterClaims, err = DecodeClusterClaims(enc) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.DidSign(clusterClaims), true, t) // signing key AssertEquals(uc2.DidSign(clusterClaims), true, t) // actual key } func testAccountWithAccountServerURL(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.AccountServerURL = u s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(oc.AccountServerURL, u, t) vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { errs := vr.Errors() return errs[0] } return nil } func Test_SystemAccount(t *testing.T) { operatorWithSystemAcc := func(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.SystemAccount = u s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(oc.SystemAccount, u, t) vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { return fmt.Errorf("%s", vr.Errors()[0]) } return nil } var asuTests = []struct { accKey string shouldFail bool }{ {"", false}, {"x", true}, {"ADZ547B24WHPLWOK7TMLNBSA7FQFXR6UM2NZ4HHNIB7RDFVZQFOZ4GQQ", false}, {"ADZ547B24WHPLWOK7TMLNBSA7FQFXR6UM2NZ4HHNIB7RDFVZQFOZ4777", true}, } for i, tt := range asuTests { err := operatorWithSystemAcc(t, tt.accKey) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].accKey) } } } func Test_AccountServerURL(t *testing.T) { var asuTests = []struct { u string shouldFail bool }{ {"", false}, {"HTTP://foo.bar.com", false}, {"http://foo.bar.com/foo/bar", false}, {"http://user:pass@foo.bar.com/foo/bar", false}, {"https://foo.bar.com", false}, {"nats://foo.bar.com", false}, {"/hello", true}, } for i, tt := range asuTests { err := testAccountWithAccountServerURL(t, tt.u) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].u) } } } func testOperatorWithOperatorServiceURL(t *testing.T, u string) error { kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) oc.OperatorServiceURLs.Add(u) s, err := oc.Encode(kp) if err != nil { return err } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } if u != "" { AssertEquals(oc.OperatorServiceURLs[0], u, t) } vr := ValidationResults{} oc.Validate(&vr) if !vr.IsEmpty() { errs := vr.Errors() return errs[0] } return nil } func Test_OperatorServiceURL(t *testing.T) { var asuTests = []struct { u string shouldFail bool }{ {"", false}, {"HTTP://foo.bar.com", true}, {"http://foo.bar.com/foo/bar", true}, {"nats://user:pass@foo.bar.com", true}, {"NATS://user:pass@foo.bar.com", true}, {"NATS://user@foo.bar.com", true}, {"nats://foo.bar.com/path", true}, {"tls://foo.bar.com/path", true}, {"/hello", true}, {"NATS://foo.bar.com", false}, {"TLS://foo.bar.com", false}, {"nats://foo.bar.com", false}, {"tls://foo.bar.com", false}, } for i, tt := range asuTests { err := testOperatorWithOperatorServiceURL(t, tt.u) if err != nil && tt.shouldFail == false { t.Fatalf("expected not to fail: %v", err) } else if err == nil && tt.shouldFail { t.Fatalf("test %s expected to fail but didn't", asuTests[i].u) } } // now test all of them in a single jwt kp := createOperatorNKey(t) pk := publicKey(kp, t) oc := NewOperatorClaims(pk) encoded := 0 shouldFail := 0 for _, v := range asuTests { oc.OperatorServiceURLs.Add(v.u) // list won't encode empty strings if v.u != "" { encoded++ } if v.shouldFail { shouldFail++ } } s, err := oc.Encode(kp) if err != nil { t.Fatal(err) } oc, err = DecodeOperatorClaims(s) if err != nil { t.Fatal(err) } AssertEquals(len(oc.OperatorServiceURLs), encoded, t) vr := ValidationResults{} oc.Validate(&vr) if vr.IsEmpty() { t.Fatal("should have had errors") } errs := vr.Errors() AssertEquals(len(errs), shouldFail, t) } func Test_ForwardCompatibility(t *testing.T) { newOp := `eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJTSUYyR0ZRSEhWWUtDQlZYRklYUURYV1FCQUcyWEw3SVZLVVJZT0ZTWlhVT0tTRUpLWDdBIiwiaWF0IjoxNTkwNTI0NTAwLCJpc3MiOiJPQlQ2REtGSzQ2STM3TjdCUkwyUkpMVVJLWUdSQTZBWVJQREFISFFFQUFBR05ZWExNR1JEUEtMQyIsInN1YiI6Ik9CVDZES0ZLNDZJMzdON0JSTDJSSkxVUktZR1JBNkFZUlBEQUhIUUVBQUFHTllYTE1HUkRQS0xDIiwibmF0cyI6eyJ0YWdzIjpbIm9uZSIsInR3byIsInRocmVlIl0sInR5cGUiOiJvcGVyYXRvciIsInZlcnNpb24iOjJ9fQ.u6JFiISIh2o-CWxktfEw3binmCLhLaFVMyuIa2HNo_x_6EGWVPVICVWc_MOLFS-6Nm17Cj4SmOh3zUtlTRkfDA` if _, err := DecodeOperatorClaims(newOp); err == nil { t.Fatal("Expected error") } else if err.Error() != `more recent jwt version` { t.Fatal("Expected different error, got: ", err) } } jwt-2.7.3/v2/v1compat/revocation_list.go000066400000000000000000000035201472706253100201520ustar00rootroot00000000000000/* * Copyright 2020 The NATS Authors * 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. */ package jwt import ( "time" ) const All = "*" // RevocationList is used to store a mapping of public keys to unix timestamps type RevocationList map[string]int64 // Revoke enters a revocation by publickey and timestamp into this export // If there is already a revocation for this public key that is newer, it is kept. func (r RevocationList) Revoke(pubKey string, timestamp time.Time) { newTS := timestamp.Unix() if ts, ok := r[pubKey]; ok && ts > newTS { return } r[pubKey] = newTS } // ClearRevocation removes any revocation for the public key func (r RevocationList) ClearRevocation(pubKey string) { delete(r, pubKey) } // IsRevoked checks if the public key is in the revoked list with a timestamp later than // the one passed in. Generally this method is called with an issue time but other time's can // be used for testing. func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool { if r.allRevoked(timestamp) { return true } ts, ok := r[pubKey] return ok && ts >= timestamp.Unix() } // allRevoked returns true if All is set and the timestamp is later or same as the // one passed. This is called by IsRevoked. func (r RevocationList) allRevoked(timestamp time.Time) bool { ts, ok := r[All] return ok && ts >= timestamp.Unix() } jwt-2.7.3/v2/v1compat/server_claims.go000066400000000000000000000047471472706253100176200ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt import ( "errors" "github.com/nats-io/nkeys" ) // Deprecated: ServerClaims are not supported type Server struct { Permissions Cluster string `json:"cluster,omitempty"` } // Validate checks the cluster and permissions for a server JWT func (s *Server) Validate(vr *ValidationResults) { if s.Cluster == "" { vr.AddError("servers can't contain an empty cluster") } } // Deprecated: ServerClaims are not supported type ServerClaims struct { ClaimsData Server `json:"nats,omitempty"` } // Deprecated: ServerClaims are not supported func NewServerClaims(subject string) *ServerClaims { if subject == "" { return nil } c := &ServerClaims{} c.Subject = subject return c } // Encode tries to turn the server claims into a JWT string func (s *ServerClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicServerKey(s.Subject) { return "", errors.New("expected subject to be a server public key") } s.ClaimsData.Type = ServerClaim return s.ClaimsData.Encode(pair, s) } // Deprecated: ServerClaims are not supported func DecodeServerClaims(token string) (*ServerClaims, error) { v := ServerClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } func (s *ServerClaims) String() string { return s.ClaimsData.String(s) } // Payload returns the server specific data func (s *ServerClaims) Payload() interface{} { return &s.Server } // Validate checks the generic and server data in the server claims func (s *ServerClaims) Validate(vr *ValidationResults) { s.ClaimsData.Validate(vr) s.Server.Validate(vr) } // ExpectedPrefixes defines the types that can encode a server JWT, operator or cluster func (s *ServerClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteOperator, nkeys.PrefixByteCluster} } // Claims returns the generic data func (s *ServerClaims) Claims() *ClaimsData { return &s.ClaimsData } jwt-2.7.3/v2/v1compat/server_claims_test.go000066400000000000000000000062141472706253100206460ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewServerClaims(t *testing.T) { ckp := createClusterNKey(t) skp := createServerNKey(t) uc := NewServerClaims(publicKey(skp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, ckp, t) uc2, err := DecodeServerClaims(uJwt) if err != nil { t.Fatal("failed to decode", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestServerClaimsIssuer(t *testing.T) { ckp := createClusterNKey(t) skp := createServerNKey(t) uc := NewServerClaims(publicKey(skp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, ckp, t) temp, err := DecodeGeneric(uJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), true}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), true}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeServerClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode server signed by %q", i.name) t.Fail() } } } func TestServerSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), true}, {"user", createUserNKey(t), false}, } for _, i := range inputs { c := NewServerClaims(publicKey(i.kp, t)) _, err := c.Encode(createOperatorNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode server with with %q subject", i.name) t.Fail() } } } func TestNewNilServerClaims(t *testing.T) { v := NewServerClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestServerType(t *testing.T) { c := NewServerClaims(publicKey(createServerNKey(t), t)) s := encode(c, createClusterNKey(t), t) u, err := DecodeServerClaims(s) if err != nil { t.Fatalf("failed to decode server claim: %v", err) } if ServerClaim != u.Type { t.Fatalf("type is unexpected %q (wanted server)", u.Type) } } jwt-2.7.3/v2/v1compat/types.go000066400000000000000000000173771472706253100161310ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "encoding/json" "fmt" "net" "strings" "time" ) // ExportType defines the type of import/export. type ExportType int const ( // Unknown is used if we don't know the type Unknown ExportType = iota // Stream defines the type field value for a stream "stream" Stream // Service defines the type field value for a service "service" Service ) func (t ExportType) String() string { switch t { case Stream: return "stream" case Service: return "service" } return "unknown" } // MarshalJSON marshals the enum as a quoted json string func (t *ExportType) MarshalJSON() ([]byte, error) { switch *t { case Stream: return []byte("\"stream\""), nil case Service: return []byte("\"service\""), nil } return nil, fmt.Errorf("unknown export type") } // UnmarshalJSON unmashals a quoted json string to the enum value func (t *ExportType) UnmarshalJSON(b []byte) error { var j string err := json.Unmarshal(b, &j) if err != nil { return err } switch j { case "stream": *t = Stream return nil case "service": *t = Service return nil } return fmt.Errorf("unknown export type") } // Subject is a string that represents a NATS subject type Subject string // Validate checks that a subject string is valid, ie not empty and without spaces func (s Subject) Validate(vr *ValidationResults) { v := string(s) if v == "" { vr.AddError("subject cannot be empty") } if strings.Contains(v, " ") { vr.AddError("subject %q cannot have spaces", v) } } // HasWildCards is used to check if a subject contains a > or * func (s Subject) HasWildCards() bool { v := string(s) return strings.HasSuffix(v, ".>") || strings.Contains(v, ".*.") || strings.HasSuffix(v, ".*") || strings.HasPrefix(v, "*.") || v == "*" || v == ">" } // IsContainedIn does a simple test to see if the subject is contained in another subject func (s Subject) IsContainedIn(other Subject) bool { otherArray := strings.Split(string(other), ".") myArray := strings.Split(string(s), ".") if len(myArray) > len(otherArray) && otherArray[len(otherArray)-1] != ">" { return false } if len(myArray) < len(otherArray) { return false } for ind, tok := range otherArray { myTok := myArray[ind] if ind == len(otherArray)-1 && tok == ">" { return true } if tok != myTok && tok != "*" { return false } } return true } // NamedSubject is the combination of a subject and a name for it type NamedSubject struct { Name string `json:"name,omitempty"` Subject Subject `json:"subject,omitempty"` } // Validate checks the subject func (ns *NamedSubject) Validate(vr *ValidationResults) { ns.Subject.Validate(vr) } // TimeRange is used to represent a start and end time type TimeRange struct { Start string `json:"start,omitempty"` End string `json:"end,omitempty"` } // Validate checks the values in a time range struct func (tr *TimeRange) Validate(vr *ValidationResults) { format := "15:04:05" if tr.Start == "" { vr.AddError("time ranges start must contain a start") } else { _, err := time.Parse(format, tr.Start) if err != nil { vr.AddError("start in time range is invalid %q", tr.Start) } } if tr.End == "" { vr.AddError("time ranges end must contain an end") } else { _, err := time.Parse(format, tr.End) if err != nil { vr.AddError("end in time range is invalid %q", tr.End) } } } // Limits are used to control acccess for users and importing accounts // Src is a comma separated list of CIDR specifications type Limits struct { Max int64 `json:"max,omitempty"` Payload int64 `json:"payload,omitempty"` Src string `json:"src,omitempty"` Times []TimeRange `json:"times,omitempty"` } // Validate checks the values in a limit struct func (l *Limits) Validate(vr *ValidationResults) { if l.Max < 0 { vr.AddError("limits cannot contain a negative maximum, %d", l.Max) } if l.Payload < 0 { vr.AddError("limits cannot contain a negative payload, %d", l.Payload) } if l.Src != "" { elements := strings.Split(l.Src, ",") for _, cidr := range elements { cidr = strings.TrimSpace(cidr) _, ipNet, err := net.ParseCIDR(cidr) if err != nil || ipNet == nil { vr.AddError("invalid cidr %q in user src limits", cidr) } } } if len(l.Times) > 0 { for _, t := range l.Times { t.Validate(vr) } } } // Permission defines allow/deny subjects type Permission struct { Allow StringList `json:"allow,omitempty"` Deny StringList `json:"deny,omitempty"` } // Validate the allow, deny elements of a permission func (p *Permission) Validate(vr *ValidationResults) { for _, subj := range p.Allow { Subject(subj).Validate(vr) } for _, subj := range p.Deny { Subject(subj).Validate(vr) } } // ResponsePermission can be used to allow responses to any reply subject // that is received on a valid subscription. type ResponsePermission struct { MaxMsgs int `json:"max"` Expires time.Duration `json:"ttl"` } // Validate the response permission. func (p *ResponsePermission) Validate(vr *ValidationResults) { // Any values can be valid for now. } // Permissions are used to restrict subject access, either on a user or for everyone on a server by default type Permissions struct { Pub Permission `json:"pub,omitempty"` Sub Permission `json:"sub,omitempty"` Resp *ResponsePermission `json:"resp,omitempty"` } // Validate the pub and sub fields in the permissions list func (p *Permissions) Validate(vr *ValidationResults) { p.Pub.Validate(vr) p.Sub.Validate(vr) if p.Resp != nil { p.Resp.Validate(vr) } } // StringList is a wrapper for an array of strings type StringList []string // Contains returns true if the list contains the string func (u *StringList) Contains(p string) bool { for _, t := range *u { if t == p { return true } } return false } // Add appends 1 or more strings to a list func (u *StringList) Add(p ...string) { for _, v := range p { if !u.Contains(v) && v != "" { *u = append(*u, v) } } } // Remove removes 1 or more strings from a list func (u *StringList) Remove(p ...string) { for _, v := range p { for i, t := range *u { if t == v { a := *u *u = append(a[:i], a[i+1:]...) break } } } } // TagList is a unique array of lower case strings // All tag list methods lower case the strings in the arguments type TagList []string // Contains returns true if the list contains the tags func (u *TagList) Contains(p string) bool { p = strings.ToLower(p) for _, t := range *u { if t == p { return true } } return false } // Add appends 1 or more tags to a list func (u *TagList) Add(p ...string) { for _, v := range p { v = strings.ToLower(v) if !u.Contains(v) && v != "" { *u = append(*u, v) } } } // Remove removes 1 or more tags from a list func (u *TagList) Remove(p ...string) { for _, v := range p { v = strings.ToLower(v) for i, t := range *u { if t == v { a := *u *u = append(a[:i], a[i+1:]...) break } } } } // Identity is used to associate an account or operator with a real entity type Identity struct { ID string `json:"id,omitempty"` Proof string `json:"proof,omitempty"` } // Validate checks the values in an Identity func (u *Identity) Validate(vr *ValidationResults) { //Fixme identity validation } jwt-2.7.3/v2/v1compat/types_test.go000066400000000000000000000136401472706253100171550ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "os" "regexp" "strings" "testing" ) func TestVersion(t *testing.T) { // Semantic versioning verRe := regexp.MustCompile(`\d+.\d+.\d+(-\S+)?`) if !verRe.MatchString(Version) { t.Fatalf("Version not compatible with semantic versioning: %q", Version) } } func TestVersionMatchesTag(t *testing.T) { tag := os.Getenv("TRAVIS_TAG") if tag == "" { t.SkipNow() } // We expect a tag of the form vX.Y.Z. If that's not the case, // we need someone to have a look. So fail if first letter is not // a `v` if len(tag) < 2 || tag[0] != 'v' { t.Fatalf("Expect tag to start with `v`, tag is: %s", tag) } // Look only at tag from current 'v', that is v1 for this file. if tag[1] != '1' { // Ignore, it is not a v1 tag. return } // Strip the `v` from the tag for the version comparison. if Version != tag[1:] { t.Fatalf("Version (%s) does not match tag (%s)", Version, tag[1:]) } } func TestTimeRangeValidation(t *testing.T) { tr := TimeRange{ Start: "hello", End: "03:15:00", } vr := CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad start should be invalid") } if !strings.Contains(vr.Issues[0].Error(), tr.Start) { t.Error("error should contain the faulty value") } tr = TimeRange{ Start: "15:43:22", End: "27:11:11", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad end should be invalid") } if !strings.Contains(vr.Issues[0].Error(), tr.End) { t.Error("error should contain the faulty value") } tr = TimeRange{ Start: "", End: "03:15:00", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad start should be invalid") } tr = TimeRange{ Start: "15:43:22", End: "", } vr = CreateValidationResults() tr.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad end should be invalid") } } func TestTagList(t *testing.T) { tags := TagList{} tags.Add("one") AssertEquals(true, tags.Contains("one"), t) AssertEquals(true, tags.Contains("ONE"), t) AssertEquals("one", tags[0], t) tags.Add("TWO") AssertEquals(true, tags.Contains("two"), t) AssertEquals(true, tags.Contains("TWO"), t) AssertEquals("two", tags[1], t) tags.Remove("ONE") AssertEquals("two", tags[0], t) AssertEquals(false, tags.Contains("one"), t) AssertEquals(false, tags.Contains("ONE"), t) } func TestStringList(t *testing.T) { slist := StringList{} slist.Add("one") AssertEquals(true, slist.Contains("one"), t) AssertEquals(false, slist.Contains("ONE"), t) AssertEquals("one", slist[0], t) slist.Add("TWO") AssertEquals(false, slist.Contains("two"), t) AssertEquals(true, slist.Contains("TWO"), t) AssertEquals("TWO", slist[1], t) slist.Remove("ONE") AssertEquals("one", slist[0], t) AssertEquals(true, slist.Contains("one"), t) AssertEquals(false, slist.Contains("ONE"), t) slist.Add("ONE") AssertEquals(true, slist.Contains("one"), t) AssertEquals(true, slist.Contains("ONE"), t) AssertEquals(3, len(slist), t) slist.Remove("one") AssertEquals("TWO", slist[0], t) AssertEquals(false, slist.Contains("one"), t) AssertEquals(true, slist.Contains("ONE"), t) } func TestSubjectValid(t *testing.T) { var s Subject vr := CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Empty string is not a valid subjects") } s = "has spaces" vr = CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Subjects cannot contain spaces") } s = "has.spa ces.and.tokens" vr = CreateValidationResults() s.Validate(vr) if !vr.IsBlocking(false) { t.Fatalf("Subjects cannot have spaces") } s = "one" vr = CreateValidationResults() s.Validate(vr) if !vr.IsEmpty() { t.Fatalf("%s is a valid subject", s) } s = "one.two.three" vr = CreateValidationResults() s.Validate(vr) if !vr.IsEmpty() { t.Fatalf("%s is a valid subject", s) } } func TestSubjectHasWildCards(t *testing.T) { s := Subject("one") AssertEquals(false, s.HasWildCards(), t) s = "one.two.three" AssertEquals(false, s.HasWildCards(), t) s = "*" AssertEquals(true, s.HasWildCards(), t) s = "one.*.three" AssertEquals(true, s.HasWildCards(), t) s = "*.two.three" AssertEquals(true, s.HasWildCards(), t) s = "one.two.*" AssertEquals(true, s.HasWildCards(), t) s = "one.>" AssertEquals(true, s.HasWildCards(), t) s = "one.two.>" AssertEquals(true, s.HasWildCards(), t) s = ">" AssertEquals(true, s.HasWildCards(), t) } func TestSubjectContainment(t *testing.T) { var s Subject var o Subject s = "one.two.three" o = "one.two.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two.*" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.*.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "*.two.three" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two.>" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.>" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = ">" AssertEquals(true, s.IsContainedIn(o), t) s = "one.two.three" o = "one.two" AssertEquals(false, s.IsContainedIn(o), t) s = "one" o = "one.two" AssertEquals(false, s.IsContainedIn(o), t) } jwt-2.7.3/v2/v1compat/user_claims.go000066400000000000000000000057551472706253100172700ustar00rootroot00000000000000/* * Copyright 2018-2019 The NATS Authors * 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. */ package jwt import ( "errors" "github.com/nats-io/nkeys" ) // User defines the user specific data in a user JWT type User struct { Permissions Limits BearerToken bool `json:"bearer_token,omitempty"` } // Validate checks the permissions and limits in a User jwt func (u *User) Validate(vr *ValidationResults) { u.Permissions.Validate(vr) u.Limits.Validate(vr) // When BearerToken is true server will ignore any nonce-signing verification } // UserClaims defines a user JWT type UserClaims struct { ClaimsData User `json:"nats,omitempty"` // IssuerAccount stores the public key for the account the issuer represents. // When set, the claim was issued by a signing key. IssuerAccount string `json:"issuer_account,omitempty"` } // NewUserClaims creates a user JWT with the specific subject/public key func NewUserClaims(subject string) *UserClaims { if subject == "" { return nil } c := &UserClaims{} c.Subject = subject return c } // Encode tries to turn the user claims into a JWT string func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) { if !nkeys.IsValidPublicUserKey(u.Subject) { return "", errors.New("expected subject to be user public key") } u.ClaimsData.Type = UserClaim return u.ClaimsData.Encode(pair, u) } // DecodeUserClaims tries to parse a user claims from a JWT string func DecodeUserClaims(token string) (*UserClaims, error) { v := UserClaims{} if err := Decode(token, &v); err != nil { return nil, err } return &v, nil } // Validate checks the generic and specific parts of the user jwt func (u *UserClaims) Validate(vr *ValidationResults) { u.ClaimsData.Validate(vr) u.User.Validate(vr) if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) { vr.AddError("account_id is not an account public key") } } // ExpectedPrefixes defines the types that can encode a user JWT, account func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte { return []nkeys.PrefixByte{nkeys.PrefixByteAccount} } // Claims returns the generic data from a user jwt func (u *UserClaims) Claims() *ClaimsData { return &u.ClaimsData } // Payload returns the user specific data from a user JWT func (u *UserClaims) Payload() interface{} { return &u.User } func (u *UserClaims) String() string { return u.ClaimsData.String(u) } // IsBearerToken returns true if nonce-signing requirements should be skipped func (u *UserClaims) IsBearerToken() bool { return u.BearerToken } jwt-2.7.3/v2/v1compat/user_claims_test.go000066400000000000000000000224531472706253100203210ustar00rootroot00000000000000/* * Copyright 2018-2020 The NATS Authors * 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. */ package jwt import ( "testing" "time" "github.com/nats-io/nkeys" ) func TestNewUserClaims(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, akp, t) uc2, err := DecodeUserClaims(uJwt) if err != nil { t.Fatal("failed to decode uc", err) } AssertEquals(uc.String(), uc2.String(), t) AssertEquals(uc.Claims() != nil, true, t) AssertEquals(uc.Payload() != nil, true, t) } func TestUserClaimIssuer(t *testing.T) { akp := createAccountNKey(t) ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() uJwt := encode(uc, akp, t) temp, err := DecodeGeneric(uJwt) if err != nil { t.Fatal("failed to decode", err) } type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), true}, {"user", createUserNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"cluster", createClusterNKey(t), false}, } for _, i := range inputs { bad := encode(temp, i.kp, t) _, err = DecodeUserClaims(bad) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to decode user signed by %q", i.name) t.Fail() } } } func TestUserSubjects(t *testing.T) { type kpInputs struct { name string kp nkeys.KeyPair ok bool } inputs := []kpInputs{ {"account", createAccountNKey(t), false}, {"cluster", createClusterNKey(t), false}, {"operator", createOperatorNKey(t), false}, {"server", createServerNKey(t), false}, {"user", createUserNKey(t), true}, } for _, i := range inputs { c := NewUserClaims(publicKey(i.kp, t)) _, err := c.Encode(createAccountNKey(t)) if i.ok && err != nil { t.Fatalf("unexpected error for %q: %v", i.name, err) } if !i.ok && err == nil { t.Logf("should have failed to encode user with with %q subject", i.name) t.Fail() } } } func TestNewNilUserClaim(t *testing.T) { v := NewUserClaims("") if v != nil { t.Fatal("expected nil user claim") } } func TestUserType(t *testing.T) { c := NewUserClaims(publicKey(createUserNKey(t), t)) s := encode(c, createAccountNKey(t), t) u, err := DecodeUserClaims(s) if err != nil { t.Fatalf("failed to decode user claim: %v", err) } if UserClaim != u.Type { t.Fatalf("user type is unexpected %q", u.Type) } } func TestSubjects(t *testing.T) { s := StringList{} if len(s) != 0 { t.Fatalf("expected len 0") } if s.Contains("a") { t.Fatalf("didn't expect 'a'") } s.Add("a") if !s.Contains("a") { t.Fatalf("expected 'a'") } s.Remove("a") if s.Contains("a") { t.Fatalf("didn't expect 'a' after removing") } } func TestUserValidation(t *testing.T) { ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Permissions.Pub.Allow.Add("a") uc.Permissions.Pub.Deny.Add("b") uc.Permissions.Sub.Allow.Add("a") uc.Permissions.Sub.Deny.Add("b") uc.Permissions.Resp = &ResponsePermission{ MaxMsgs: 10, Expires: 50 * time.Minute, } uc.Limits.Max = 10 uc.Limits.Payload = 10 uc.Limits.Src = "192.0.2.0/24" uc.Limits.Times = []TimeRange{ { Start: "01:15:00", End: "03:15:00", }, { Start: "06:15:00", End: "09:15:00", }, } vr := CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("valid user permissions should be valid") } uc.Limits.Max = -1 vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Max = 10 uc.Limits.Payload = -1 vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Payload = 10 uc.Limits.Src = "hello world" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Payload = 10 uc.Limits.Src = "hello world" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } tr := TimeRange{ Start: "hello", End: "03:15:00", } uc.Limits.Src = "192.0.2.0/24" uc.Limits.Times = append(uc.Limits.Times, tr) vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad limit should be invalid") } uc.Limits.Times = []TimeRange{} uc.Permissions.Pub.Allow.Add("bad subject") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad permission should be invalid") } uc.Permissions.Pub.Allow.Remove("bad subject") uc.Permissions.Sub.Allow.Add("bad subject") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad permission should be invalid") } uc.Permissions.Sub.Allow.Remove("bad subject") uc.Permissions.Pub.Deny.Add("bad subject") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad permission should be invalid") } uc.Permissions.Pub.Deny.Remove("bad subject") uc.Permissions.Sub.Deny.Add("bad subject") vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { t.Error("bad permission should be invalid") } } func TestUserAccountID(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) a2kp := createAccountNKey(t) ac := NewAccountClaims(apk) ac.SigningKeys.Add(publicKey(a2kp, t)) token, err := ac.Encode(akp) if err != nil { t.Fatal(err) } ac, err = DecodeAccountClaims(token) if err != nil { t.Fatal(err) } uc := NewUserClaims(publicKey(createUserNKey(t), t)) uc.IssuerAccount = apk userToken, err := uc.Encode(a2kp) if err != nil { t.Fatal(err) } uc, err = DecodeUserClaims(userToken) if err != nil { t.Fatal(err) } if uc.IssuerAccount != apk { t.Fatalf("expected AccountID to be set to %s - got %s", apk, uc.IssuerAccount) } signed := ac.DidSign(uc) if !signed { t.Fatal("expected user signed by account") } } func TestUserAccountIDValidation(t *testing.T) { uc := NewUserClaims(publicKey(createUserNKey(t), t)) uc.IssuerAccount = publicKey(createAccountNKey(t), t) var vr ValidationResults uc.Validate(&vr) if len(vr.Issues) != 0 { t.Fatal("expected no issues") } uc.IssuerAccount = publicKey(createUserNKey(t), t) uc.Validate(&vr) if len(vr.Issues) != 1 { t.Fatal("expected validation issues") } } func TestSourceNetworkValidation(t *testing.T) { ukp := createUserNKey(t) uc := NewUserClaims(publicKey(ukp, t)) uc.Limits.Src = "192.0.2.0/24" vr := CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = "192.0.2.1/1" vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = "192.0.2.0/24,2001:db8:a0b:12f0::1/32" vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = "192.0.2.0/24 ,\t2001:db8:a0b:12f0::1/32 , 192.168.1.1/1" vr = CreateValidationResults() uc.Validate(vr) if !vr.IsEmpty() { t.Error("limits should be valid") } uc.Limits.Src = "foo" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 { t.Error("limits should be invalid") } uc.Limits.Src = "192.0.2.0/24,foo" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 1 { t.Error("limits should be invalid") } uc.Limits.Src = "bloo,foo" vr = CreateValidationResults() uc.Validate(vr) if vr.IsEmpty() || len(vr.Issues) != 2 { t.Error("limits should be invalid") } } func TestUserClaimRevocation(t *testing.T) { akp := createAccountNKey(t) apk := publicKey(akp, t) account := NewAccountClaims(apk) u := publicKey(createUserNKey(t), t) aminAgo := time.Now().Add(-time.Minute) if account.Revocations.IsRevoked(u, aminAgo) { t.Fatal("shouldn't be revoked") } account.RevokeAt(u, aminAgo) if !account.Revocations.IsRevoked(u, aminAgo) { t.Fatal("should be revoked") } u2 := publicKey(createUserNKey(t), t) if account.Revocations.IsRevoked(u2, aminAgo) { t.Fatal("should not be revoked") } account.RevokeAt("*", aminAgo) if !account.Revocations.IsRevoked(u2, time.Now().Add(-time.Hour)) { t.Fatal("should be revoked") } vr := ValidationResults{} account.Validate(&vr) if !vr.IsEmpty() { t.Fatal("account validation shouldn't have failed") } } jwt-2.7.3/v2/v1compat/util_test.go000066400000000000000000000047061472706253100167710ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" "runtime" "strings" "testing" "github.com/nats-io/nkeys" ) func Trace(message string) string { lines := make([]string, 0, 32) err := errors.New(message) msg := err.Error() lines = append(lines, msg) for i := 2; true; i++ { _, file, line, ok := runtime.Caller(i) if !ok { break } msg := fmt.Sprintf("%s:%d", file, line) lines = append(lines, msg) } return strings.Join(lines, "\n") } func AssertEquals(expected, v interface{}, t *testing.T) { if expected != v { t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) } } func createAccountNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateAccount() if err != nil { t.Fatal("error creating account kp", err) } return kp } func createUserNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateUser() if err != nil { t.Fatal("error creating account kp", err) } return kp } func createOperatorNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateOperator() if err != nil { t.Fatal("error creating operator kp", err) } return kp } func createServerNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateServer() if err != nil { t.Fatal("error creating server kp", err) } return kp } func createClusterNKey(t *testing.T) nkeys.KeyPair { kp, err := nkeys.CreateCluster() if err != nil { t.Fatal("error creating cluster kp", err) } return kp } func publicKey(kp nkeys.KeyPair, t *testing.T) string { pk, err := kp.PublicKey() if err != nil { t.Fatal("error reading public key", err) } return string(pk) } func seedKey(kp nkeys.KeyPair, t *testing.T) []byte { sk, err := kp.Seed() if err != nil { t.Fatal("error reading seed", err) } return sk } func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { s, err := c.Encode(kp) if err != nil { t.Fatal("error encoding claim", err) } return s } jwt-2.7.3/v2/v1compat/validation.go000066400000000000000000000057451472706253100171130ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" ) // ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error type ValidationIssue struct { Description string Blocking bool TimeCheck bool } func (ve *ValidationIssue) Error() string { return ve.Description } // ValidationResults is a list of ValidationIssue pointers type ValidationResults struct { Issues []*ValidationIssue } // CreateValidationResults creates an empty list of validation issues func CreateValidationResults() *ValidationResults { issues := []*ValidationIssue{} return &ValidationResults{ Issues: issues, } } // Add appends an issue to the list func (v *ValidationResults) Add(vi *ValidationIssue) { v.Issues = append(v.Issues, vi) } // AddError creates a new validation error and adds it to the list func (v *ValidationResults) AddError(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: true, TimeCheck: false, }) } // AddTimeCheck creates a new validation issue related to a time check and adds it to the list func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: false, TimeCheck: true, }) } // AddWarning creates a new validation warning and adds it to the list func (v *ValidationResults) AddWarning(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: false, TimeCheck: false, }) } // IsBlocking returns true if the list contains a blocking error func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool { for _, i := range v.Issues { if i.Blocking { return true } if includeTimeChecks && i.TimeCheck { return true } } return false } // IsEmpty returns true if the list is empty func (v *ValidationResults) IsEmpty() bool { return len(v.Issues) == 0 } // Errors returns only blocking issues as errors func (v *ValidationResults) Errors() []error { var errs []error for _, v := range v.Issues { if v.Blocking { errs = append(errs, errors.New(v.Description)) } } return errs } // Warnings returns only non blocking issues as strings func (v *ValidationResults) Warnings() []string { var errs []string for _, v := range v.Issues { if !v.Blocking { errs = append(errs, v.Description) } } return errs } jwt-2.7.3/v2/validation.go000066400000000000000000000057441472706253100153600ustar00rootroot00000000000000/* * Copyright 2018 The NATS Authors * 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. */ package jwt import ( "errors" "fmt" ) // ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error type ValidationIssue struct { Description string Blocking bool TimeCheck bool } func (ve *ValidationIssue) Error() string { return ve.Description } // ValidationResults is a list of ValidationIssue pointers type ValidationResults struct { Issues []*ValidationIssue } // CreateValidationResults creates an empty list of validation issues func CreateValidationResults() *ValidationResults { var issues []*ValidationIssue return &ValidationResults{ Issues: issues, } } // Add appends an issue to the list func (v *ValidationResults) Add(vi *ValidationIssue) { v.Issues = append(v.Issues, vi) } // AddError creates a new validation error and adds it to the list func (v *ValidationResults) AddError(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: true, TimeCheck: false, }) } // AddTimeCheck creates a new validation issue related to a time check and adds it to the list func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: false, TimeCheck: true, }) } // AddWarning creates a new validation warning and adds it to the list func (v *ValidationResults) AddWarning(format string, args ...interface{}) { v.Add(&ValidationIssue{ Description: fmt.Sprintf(format, args...), Blocking: false, TimeCheck: false, }) } // IsBlocking returns true if the list contains a blocking error func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool { for _, i := range v.Issues { if i.Blocking { return true } if includeTimeChecks && i.TimeCheck { return true } } return false } // IsEmpty returns true if the list is empty func (v *ValidationResults) IsEmpty() bool { return len(v.Issues) == 0 } // Errors returns only blocking issues as errors func (v *ValidationResults) Errors() []error { var errs []error for _, v := range v.Issues { if v.Blocking { errs = append(errs, errors.New(v.Description)) } } return errs } // Warnings returns only non blocking issues as strings func (v *ValidationResults) Warnings() []string { var errs []string for _, v := range v.Issues { if !v.Blocking { errs = append(errs, v.Description) } } return errs }