├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── defect.yml │ └── proposal.yml └── workflows │ └── go-test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── ReleaseNotes.md ├── dependencies.md ├── scripts ├── cov.sh └── test.sh └── v2 ├── Makefile ├── account_claims.go ├── account_claims_test.go ├── activation_claims.go ├── activation_claims_test.go ├── authorization_claims.go ├── authorization_claims_test.go ├── claims.go ├── creds_utils.go ├── creds_utils_test.go ├── decoder.go ├── decoder_account.go ├── decoder_activation.go ├── decoder_authorization.go ├── decoder_operator.go ├── decoder_test.go ├── decoder_user.go ├── example_test.go ├── exports.go ├── exports_test.go ├── genericlaims.go ├── go.mod ├── go.sum ├── header.go ├── imports.go ├── imports_test.go ├── operator_claims.go ├── operator_claims_test.go ├── revocation_list.go ├── revocation_list_test.go ├── signingkeys.go ├── signingkeys_test.go ├── test ├── decoder_migration_test.go ├── genericclaims_test.go └── util_test.go ├── types.go ├── types_test.go ├── user_claims.go ├── user_claims_test.go ├── util_test.go ├── v1compat ├── Makefile ├── account_claims.go ├── account_claims_test.go ├── activation_claims.go ├── activation_claims_test.go ├── claims.go ├── cluster_claims.go ├── cluster_claims_test.go ├── creds_utils.go ├── creds_utils_test.go ├── decoder_test.go ├── exports.go ├── exports_test.go ├── genericclaims_test.go ├── genericlaims.go ├── header.go ├── imports.go ├── imports_test.go ├── operator_claims.go ├── operator_claims_test.go ├── revocation_list.go ├── server_claims.go ├── server_claims_test.go ├── types.go ├── types_test.go ├── user_claims.go ├── user_claims_test.go ├── util_test.go └── validation.go └── validation.go /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discussion 4 | url: https://github.com/nats-io/jwt/discussions 5 | about: Ideal for ideas, feedback, or longer form questions. 6 | - name: Chat 7 | url: https://slack.nats.io 8 | about: Ideal for short, one-off questions, general conversation, and meeting other NATS users! 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/defect.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Defect 3 | description: Report a defect, such as a bug or regression. 4 | labels: 5 | - defect 6 | body: 7 | - type: textarea 8 | id: versions 9 | attributes: 10 | label: What version were you using? 11 | description: Include the server version (`nats-server --version`) and any client versions when observing the issue. 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: environment 16 | attributes: 17 | label: What environment was the server running in? 18 | description: This pertains to the operating system, CPU architecture, and/or Docker image that was used. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: steps 23 | attributes: 24 | label: Is this defect reproducible? 25 | description: Provide best-effort steps to showcase the defect. 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected 30 | attributes: 31 | label: Given the capability you are leveraging, describe your expectation? 32 | description: This may be the expected behavior or performance characteristics. 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: actual 37 | attributes: 38 | label: Given the expectation, what is the defect you are observing? 39 | description: This may be an unexpected behavior or regression in performance. 40 | validations: 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposal 3 | description: Propose an enhancement or new feature. 4 | labels: 5 | - proposal 6 | body: 7 | - type: textarea 8 | id: usecase 9 | attributes: 10 | label: What motivated this proposal? 11 | description: Describe the use case justifying this request. 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: change 16 | attributes: 17 | label: What is the proposed change? 18 | description: This could be a behavior change, enhanced API, or a branch new feature. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: benefits 23 | attributes: 24 | label: Who benefits from this change? 25 | description: Describe how this not only benefits you. 26 | validations: 27 | required: false 28 | - type: textarea 29 | id: alternates 30 | attributes: 31 | label: What alternatives have you evaluated? 32 | description: This could be using existing features or relying on an external dependency. 33 | validations: 34 | required: false 35 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yaml: -------------------------------------------------------------------------------- 1 | name: jwt testing 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: ${{ matrix.config.kind }} ${{ matrix.config.os }} 7 | strategy: 8 | matrix: 9 | include: 10 | - go: stable 11 | os: ubuntu-latest 12 | canonical: true 13 | - go: stable 14 | os: windows-latest 15 | canonical: false 16 | 17 | env: 18 | GO111MODULE: "on" 19 | 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 1 26 | 27 | - name: Setup Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: ${{matrix.go}} 31 | 32 | - name: Install deps 33 | shell: bash --noprofile --norc -x -eo pipefail {0} 34 | run: | 35 | go install honnef.co/go/tools/cmd/staticcheck@latest 36 | go install github.com/client9/misspell/cmd/misspell@latest 37 | go install github.com/wadey/gocovmerge@latest 38 | 39 | - name: Lint 40 | shell: bash --noprofile --norc -x -eo pipefail {0} 41 | run: | 42 | cd v2 43 | GO_LIST=$(go list ./...) 44 | go build 45 | $(exit $(go fmt $GO_LIST | wc -l)) 46 | go vet $GO_LIST 47 | which misspell 48 | find . -type f -name "*.go" | xargs misspell -error -locale US 49 | staticcheck $GO_LIST 50 | if: matrix.canonical 51 | 52 | - name: Tests 53 | shell: bash --noprofile --norc -x -eo pipefail {0} 54 | run: | 55 | set -e 56 | mkdir -p cov 57 | cd v2 58 | go get -t ./... 59 | go test -v -race -covermode=atomic -coverprofile=../cov/v2.out -coverpkg=github.com/nats-io/jwt/v2 . 60 | cd v1compat 61 | go test -v -race -covermode=atomic -coverprofile=../../cov/v1.out -coverpkg=github.com/nats-io/jwt/v2/v1compat . 62 | cd ../.. 63 | gocovmerge ./cov/*.out > ./coverage.out 64 | set +e 65 | if: matrix.canonical 66 | 67 | - name: Tests (Windows) 68 | shell: bash --noprofile --norc -x -eo pipefail {0} 69 | run: | 70 | set -e 71 | cd v2 72 | go get -t ./... 73 | go test -v -race . 74 | set +e 75 | if: runner.os == 'Windows' 76 | 77 | - name: Coverage 78 | uses: shogo82148/actions-goveralls@v1 79 | with: 80 | # this needs to be where it can find a go.mod 81 | working-directory: ./v2 82 | # this path-to-profile is relative to the working-directory 83 | path-to-profile: ../coverage.out 84 | if: matrix.canonical 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDE Files 15 | .vscode 16 | .idea/ 17 | 18 | coverage.out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT 2 | A [JWT](https://jwt.io/) implementation that uses [nkeys](https://github.com/nats-io/nkeys) to digitally sign JWT tokens. 3 | Nkeys use [Ed25519](https://ed25519.cr.yp.to/) to provide authentication of JWT claims. 4 | 5 | 6 | [![License Apache 2](https://img.shields.io/badge/License-Apache2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 7 | [![ReportCard](https://goreportcard.com/badge/github.com/nats-io/jwt)](https://goreportcard.com/report/nats-io/jwt) 8 | [![Build Status](https://travis-ci.com/nats-io/jwt.svg?branch=master)](https://travis-ci.com/github/nats-io/jwt) 9 | [![GoDoc](https://godoc.org/github.com/nats-io/jwt?status.png)](https://godoc.org/github.com/nats-io/jwt/v2) 10 | [![Coverage Status](https://coveralls.io/repos/github/nats-io/jwt/badge.svg?branch=main)](https://coveralls.io/github/nats-io/jwt?branch=main) 11 | ```go 12 | // create an operator key pair (private key) 13 | okp, err := nkeys.CreateOperator() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | // extract the public key 18 | opk, err := okp.PublicKey() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | // create an operator claim using the public key for the identifier 24 | oc := jwt.NewOperatorClaims(opk) 25 | oc.Name = "O" 26 | // add an operator signing key to sign accounts 27 | oskp, err := nkeys.CreateOperator() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | // get the public key for the signing key 32 | ospk, err := oskp.PublicKey() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | // add the signing key to the operator - this makes any account 37 | // issued by the signing key to be valid for the operator 38 | oc.SigningKeys.Add(ospk) 39 | 40 | // self-sign the operator JWT - the operator trusts itself 41 | operatorJWT, err := oc.Encode(okp) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | // create an account keypair 47 | akp, err := nkeys.CreateAccount() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | // extract the public key for the account 52 | apk, err := akp.PublicKey() 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | // create the claim for the account using the public key of the account 57 | ac := jwt.NewAccountClaims(apk) 58 | ac.Name = "A" 59 | // create a signing key that we can use for issuing users 60 | askp, err := nkeys.CreateAccount() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | // extract the public key 65 | aspk, err := askp.PublicKey() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | // add the signing key (public) to the account 70 | ac.SigningKeys.Add(aspk) 71 | 72 | // now we could encode an issue the account using the operator 73 | // key that we generated above, but this will illustrate that 74 | // the account could be self-signed, and given to the operator 75 | // who can then re-sign it 76 | accountJWT, err := ac.Encode(akp) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | // the operator would decode the provided token, if the token 82 | // is not self-signed or signed by an operator or tampered with 83 | // the decoding would fail 84 | ac, err = jwt.DecodeAccountClaims(accountJWT) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | // here the operator is going to use its private signing key to 89 | // re-issue the account 90 | accountJWT, err = ac.Encode(oskp) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | // now back to the account, the account can issue users 96 | // need not be known to the operator - the users are trusted 97 | // because they will be signed by the account. The server will 98 | // look up the account get a list of keys the account has and 99 | // verify that the user was issued by one of those keys 100 | ukp, err := nkeys.CreateUser() 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | upk, err := ukp.PublicKey() 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | uc := jwt.NewUserClaims(upk) 109 | // since the jwt will be issued by a signing key, the issuer account 110 | // must be set to the public ID of the account 111 | uc.IssuerAccount = apk 112 | userJwt, err := uc.Encode(askp) 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | // the seed is a version of the keypair that is stored as text 117 | useed, err := ukp.Seed() 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | // generate a creds formatted file that can be used by a NATS client 122 | creds, err := jwt.FormatUserConfig(userJwt, useed) 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | 127 | // now we are going to put it together into something that can be run 128 | // we create a directory to store the server configuration, the creds 129 | // file and a small go program that uses the creds file 130 | dir, err := os.MkdirTemp(os.TempDir(), "jwt_example") 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | // print where we generated the file 135 | t.Logf("generated example %s", dir) 136 | t.Log("to run this example:") 137 | t.Logf("> cd %s", dir) 138 | t.Log("> go mod init example") 139 | t.Log("> go mod tidy") 140 | t.Logf("> nats-server -c %s/resolver.conf &", dir) 141 | t.Log("> go run main.go") 142 | 143 | // we are generating a memory resolver server configuration 144 | // it lists the operator and all account jwts the server should 145 | // know about 146 | resolver := fmt.Sprintf(`operator: %s 147 | 148 | resolver: MEMORY 149 | resolver_preload: { 150 | %s: %s 151 | } 152 | `, operatorJWT, apk, accountJWT) 153 | if err := os.WriteFile(path.Join(dir, "resolver.conf"), 154 | []byte(resolver), 0644); err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | // store the creds 159 | credsPath := path.Join(dir, "u.creds") 160 | if err := os.WriteFile(credsPath, creds, 0644); err != nil { 161 | t.Fatal(err) 162 | } 163 | 164 | // here we generate as small go program that connects using the creds file 165 | // subscribes, and publishes a message 166 | connect := fmt.Sprintf(` 167 | package main 168 | 169 | import ( 170 | "fmt" 171 | "sync" 172 | 173 | "github.com/nats-io/nats.go" 174 | ) 175 | 176 | func main() { 177 | var wg sync.WaitGroup 178 | wg.Add(1) 179 | nc, err := nats.Connect(nats.DefaultURL, nats.UserCredentials(%q)) 180 | if err != nil { 181 | panic(err) 182 | } 183 | nc.Subscribe("hello.world", func(m *nats.Msg) { 184 | fmt.Println(m.Subject) 185 | wg.Done() 186 | }) 187 | nc.Publish("hello.world", []byte("hello")) 188 | nc.Flush() 189 | wg.Wait() 190 | nc.Close() 191 | } 192 | 193 | `, credsPath) 194 | if err := os.WriteFile(path.Join(dir, "main.go"), []byte(connect), 0644); err != nil { 195 | t.Fatal(err) 196 | } 197 | ``` -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 0.3.0 4 | 5 | * Removed revocation claims in favor of timestamp-based revocation maps in account and export claims. 6 | -------------------------------------------------------------------------------- /dependencies.md: -------------------------------------------------------------------------------- 1 | # External Dependencies 2 | 3 | This file lists the dependencies used in this repository. 4 | 5 | | Dependency | License | 6 | |-|-| 7 | | github.com/nats-io/nkeys | Apache License 2.0 | 8 | | go | BSD 3-Clause "New" or "Revised" License | 9 | -------------------------------------------------------------------------------- /scripts/cov.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Run from directory above via ./scripts/cov.sh 3 | 4 | rm -rf ./cov 5 | mkdir cov 6 | go test -v -race -covermode=atomic -coverprofile=./cov/coverage.out -coverpkg=github.com/nats-io/jwt . 7 | gocovmerge ./cov/*.out > coverage.out 8 | rm -rf ./cov 9 | 10 | # If we have an arg, assume travis run and push to coveralls. Otherwise launch browser results 11 | if [[ -n $1 ]]; then 12 | $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service travis-ci 13 | rm -rf ./coverage.out 14 | else 15 | go tool cover -html=coverage.out 16 | fi -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Run from directory above via ./scripts/test.sh 3 | 4 | gofmt -s -w *.go 5 | goimports -w *.go 6 | go vet ./... 7 | go test -v 8 | go test -v --race 9 | 10 | cd v2 && ( 11 | gofmt -s -w *.go 12 | goimports -w *.go 13 | go vet -modfile=go_test.mod ./... 14 | go test github.com/nats-io/jwt/v2 -v 15 | go test github.com/nats-io/jwt/v2 -v --race 16 | go test -modfile=go_test.mod github.com/nats-io/jwt/v2/test -v 17 | go test -modfile=go_test.mod github.com/nats-io/jwt/v2/test -v --race 18 | ) 19 | 20 | staticcheck ./... 21 | -------------------------------------------------------------------------------- /v2/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover 2 | 3 | build: 4 | go build 5 | 6 | fmt: 7 | gofmt -w -s *.go 8 | goimports -w *.go 9 | go mod tidy 10 | 11 | test: 12 | go vet ./... 13 | staticcheck ./... 14 | rm -rf ./coverage.out 15 | go test -v -coverprofile=./coverage.out ./... 16 | 17 | cover: 18 | go tool cover -html=coverage.out -------------------------------------------------------------------------------- /v2/activation_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "crypto/sha256" 20 | "encoding/base32" 21 | "errors" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | // Activation defines the custom parts of an activation claim 29 | type Activation struct { 30 | ImportSubject Subject `json:"subject,omitempty"` 31 | ImportType ExportType `json:"kind,omitempty"` 32 | // IssuerAccount stores the public key for the account the issuer represents. 33 | // When set, the claim was issued by a signing key. 34 | IssuerAccount string `json:"issuer_account,omitempty"` 35 | GenericFields 36 | } 37 | 38 | // IsService returns true if an Activation is for a service 39 | func (a *Activation) IsService() bool { 40 | return a.ImportType == Service 41 | } 42 | 43 | // IsStream returns true if an Activation is for a stream 44 | func (a *Activation) IsStream() bool { 45 | return a.ImportType == Stream 46 | } 47 | 48 | // Validate checks the exports and limits in an activation JWT 49 | func (a *Activation) Validate(vr *ValidationResults) { 50 | if !a.IsService() && !a.IsStream() { 51 | vr.AddError("invalid import type: %q", a.ImportType) 52 | } 53 | 54 | a.ImportSubject.Validate(vr) 55 | } 56 | 57 | // ActivationClaims holds the data specific to an activation JWT 58 | type ActivationClaims struct { 59 | ClaimsData 60 | Activation `json:"nats,omitempty"` 61 | } 62 | 63 | // NewActivationClaims creates a new activation claim with the provided sub 64 | func NewActivationClaims(subject string) *ActivationClaims { 65 | if subject == "" { 66 | return nil 67 | } 68 | ac := &ActivationClaims{} 69 | ac.Subject = subject 70 | return ac 71 | } 72 | 73 | // Encode turns an activation claim into a JWT strimg 74 | func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) { 75 | return a.EncodeWithSigner(pair, nil) 76 | } 77 | 78 | func (a *ActivationClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { 79 | if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) { 80 | return "", errors.New("expected subject to be an account") 81 | } 82 | a.Type = ActivationClaim 83 | return a.ClaimsData.encode(pair, a, fn) 84 | } 85 | 86 | // DecodeActivationClaims tries to create an activation claim from a JWT string 87 | func DecodeActivationClaims(token string) (*ActivationClaims, error) { 88 | claims, err := Decode(token) 89 | if err != nil { 90 | return nil, err 91 | } 92 | ac, ok := claims.(*ActivationClaims) 93 | if !ok { 94 | return nil, errors.New("not activation claim") 95 | } 96 | return ac, nil 97 | } 98 | 99 | // Payload returns the activation specific part of the JWT 100 | func (a *ActivationClaims) Payload() interface{} { 101 | return a.Activation 102 | } 103 | 104 | // Validate checks the claims 105 | func (a *ActivationClaims) Validate(vr *ValidationResults) { 106 | a.validateWithTimeChecks(vr, true) 107 | } 108 | 109 | // Validate checks the claims 110 | func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) { 111 | if timeChecks { 112 | a.ClaimsData.Validate(vr) 113 | } 114 | a.Activation.Validate(vr) 115 | if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) { 116 | vr.AddError("account_id is not an account public key") 117 | } 118 | } 119 | 120 | func (a *ActivationClaims) ClaimType() ClaimType { 121 | return a.Type 122 | } 123 | 124 | func (a *ActivationClaims) updateVersion() { 125 | a.GenericFields.Version = libVersion 126 | } 127 | 128 | // ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator 129 | func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte { 130 | return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} 131 | } 132 | 133 | // Claims returns the generic part of the JWT 134 | func (a *ActivationClaims) Claims() *ClaimsData { 135 | return &a.ClaimsData 136 | } 137 | 138 | func (a *ActivationClaims) String() string { 139 | return a.ClaimsData.String(a) 140 | } 141 | 142 | // HashID returns a hash of the claims that can be used to identify it. 143 | // The hash is calculated by creating a string with 144 | // issuerPubKey.subjectPubKey. and constructing the sha-256 hash and base32 encoding that. 145 | // is the exported subject, minus any wildcards, so foo.* becomes foo. 146 | // the one special case is that if the export start with "*" or is ">" the "_" 147 | func (a *ActivationClaims) HashID() (string, error) { 148 | 149 | if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" { 150 | return "", fmt.Errorf("not enough data in the activaion claims to create a hash") 151 | } 152 | 153 | subject := cleanSubject(string(a.ImportSubject)) 154 | base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject) 155 | h := sha256.New() 156 | h.Write([]byte(base)) 157 | sha := h.Sum(nil) 158 | hash := base32.StdEncoding.EncodeToString(sha) 159 | 160 | return hash, nil 161 | } 162 | 163 | func cleanSubject(subject string) string { 164 | split := strings.Split(subject, ".") 165 | cleaned := "" 166 | 167 | for i, tok := range split { 168 | if tok == "*" || tok == ">" { 169 | if i == 0 { 170 | cleaned = "_" 171 | break 172 | } 173 | 174 | cleaned = strings.Join(split[:i], ".") 175 | break 176 | } 177 | } 178 | if cleaned == "" { 179 | cleaned = subject 180 | } 181 | return cleaned 182 | } 183 | -------------------------------------------------------------------------------- /v2/authorization_claims_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/nats-io/nkeys" 22 | ) 23 | 24 | func TestNewAuthorizationRequestClaims(t *testing.T) { 25 | skp, _ := nkeys.CreateServer() 26 | 27 | kp, err := nkeys.CreateUser() 28 | if err != nil { 29 | t.Fatalf("Error creating user: %v", err) 30 | } 31 | pub, _ := kp.PublicKey() 32 | 33 | // the subject of the claim is the user we are generating an authorization response 34 | ac := NewAuthorizationRequestClaims(pub) 35 | ac.Server.Name = "NATS-1" 36 | 37 | vr := CreateValidationResults() 38 | 39 | // Make sure that user nkey is required. 40 | ac.Validate(vr) 41 | if vr.IsEmpty() || !vr.IsBlocking(false) { 42 | t.Fatalf("Expected blocking error on an nkey user not being specified") 43 | } 44 | 45 | // Make sure it is required to be valid public user nkey. 46 | ac.UserNkey = "derek" 47 | vr = CreateValidationResults() 48 | ac.Validate(vr) 49 | if vr.IsEmpty() || !vr.IsBlocking(false) { 50 | t.Fatalf("Expected blocking error on invalid user nkey") 51 | } 52 | 53 | ac.UserNkey = pub 54 | vr = CreateValidationResults() 55 | ac.Validate(vr) 56 | if !vr.IsEmpty() { 57 | t.Fatal("Valid authorization request will have no validation results") 58 | } 59 | 60 | acJWT := encode(ac, skp, t) 61 | 62 | ac2, err := DecodeAuthorizationRequestClaims(acJWT) 63 | if err != nil { 64 | t.Fatal("error decoding authorization request jwt", err) 65 | } 66 | 67 | AssertEquals(ac.String(), ac2.String(), t) 68 | AssertEquals(ac.Server.Name, ac2.Server.Name, t) 69 | } 70 | 71 | func TestAuthorizationResponse_EmptyShouldFail(t *testing.T) { 72 | rc := NewAuthorizationResponseClaims("$G") 73 | vr := CreateValidationResults() 74 | rc.Validate(vr) 75 | if vr.IsEmpty() || !vr.IsBlocking(false) { 76 | t.Fatal("Expected blocking errors") 77 | } 78 | errs := vr.Errors() 79 | AssertEquals(3, len(errs), t) 80 | AssertEquals("Subject must be a user public key", errs[0].Error(), t) 81 | AssertEquals("Audience must be a server public key", errs[1].Error(), t) 82 | AssertEquals("Error or Jwt is required", errs[2].Error(), t) 83 | } 84 | 85 | func TestAuthorizationResponse_SubjMustBeServer(t *testing.T) { 86 | rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) 87 | rc.Error = "bad" 88 | vr := CreateValidationResults() 89 | rc.Validate(vr) 90 | if vr.IsEmpty() || !vr.IsBlocking(false) { 91 | t.Fatal("Expected blocking errors") 92 | } 93 | errs := vr.Errors() 94 | AssertEquals(1, len(errs), t) 95 | AssertEquals("Audience must be a server public key", errs[0].Error(), t) 96 | 97 | rc = NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) 98 | rc.Audience = publicKey(createServerNKey(t), t) 99 | rc.Error = "bad" 100 | vr = CreateValidationResults() 101 | rc.Validate(vr) 102 | AssertEquals(true, vr.IsEmpty(), t) 103 | } 104 | 105 | func TestAuthorizationResponse_OneOfErrOrJwt(t *testing.T) { 106 | rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) 107 | rc.Audience = publicKey(createServerNKey(t), t) 108 | rc.Error = "bad" 109 | rc.Jwt = "jwt" 110 | vr := CreateValidationResults() 111 | rc.Validate(vr) 112 | if vr.IsEmpty() || !vr.IsBlocking(false) { 113 | t.Fatal("Expected blocking errors") 114 | } 115 | errs := vr.Errors() 116 | AssertEquals(1, len(errs), t) 117 | AssertEquals("Only Error or Jwt can be set", errs[0].Error(), t) 118 | } 119 | 120 | func TestAuthorizationResponse_IssuerAccount(t *testing.T) { 121 | rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) 122 | rc.Audience = publicKey(createServerNKey(t), t) 123 | rc.Jwt = "jwt" 124 | rc.IssuerAccount = rc.Subject 125 | vr := CreateValidationResults() 126 | rc.Validate(vr) 127 | if vr.IsEmpty() || !vr.IsBlocking(false) { 128 | t.Fatal("Expected blocking errors") 129 | } 130 | errs := vr.Errors() 131 | AssertEquals(1, len(errs), t) 132 | AssertEquals("issuer_account is not an account public key", errs[0].Error(), t) 133 | 134 | akp := createAccountNKey(t) 135 | rc.IssuerAccount = publicKey(akp, t) 136 | vr = CreateValidationResults() 137 | rc.Validate(vr) 138 | AssertEquals(true, vr.IsEmpty(), t) 139 | } 140 | 141 | func TestAuthorizationResponse_Decode(t *testing.T) { 142 | rc := NewAuthorizationResponseClaims(publicKey(createUserNKey(t), t)) 143 | rc.Audience = publicKey(createServerNKey(t), t) 144 | rc.Jwt = "jwt" 145 | akp := createAccountNKey(t) 146 | tok, err := rc.Encode(akp) 147 | AssertNoError(err, t) 148 | 149 | r, err := DecodeAuthorizationResponseClaims(tok) 150 | AssertNoError(err, t) 151 | vr := CreateValidationResults() 152 | r.Validate(vr) 153 | AssertEquals(true, vr.IsEmpty(), t) 154 | AssertEquals("jwt", r.Jwt, t) 155 | AssertTrue(nkeys.IsValidPublicUserKey(r.Subject), t) 156 | AssertTrue(nkeys.IsValidPublicServerKey(r.Audience), t) 157 | } 158 | 159 | func TestNewAuthorizationRequestSignerFn(t *testing.T) { 160 | skp, _ := nkeys.CreateServer() 161 | 162 | kp, err := nkeys.CreateUser() 163 | if err != nil { 164 | t.Fatalf("Error creating user: %v", err) 165 | } 166 | 167 | // the subject of the claim is the user we are generating an authorization response 168 | ac := NewAuthorizationRequestClaims(publicKey(kp, t)) 169 | ac.Server.Name = "NATS-1" 170 | ac.UserNkey = publicKey(kp, t) 171 | 172 | ok := false 173 | ar, err := ac.EncodeWithSigner(skp, func(pub string, data []byte) ([]byte, error) { 174 | ok = true 175 | return skp.Sign(data) 176 | }) 177 | if err != nil { 178 | t.Fatal("error signing request") 179 | } 180 | if !ok { 181 | t.Fatal("not signed by signer function") 182 | } 183 | 184 | ac2, err := DecodeAuthorizationRequestClaims(ar) 185 | if err != nil { 186 | t.Fatal("error decoding authorization request jwt", err) 187 | } 188 | 189 | vr := CreateValidationResults() 190 | ac2.Validate(vr) 191 | if !vr.IsEmpty() { 192 | t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /v2/claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "crypto/sha512" 20 | "encoding/base32" 21 | "encoding/base64" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "time" 26 | 27 | "github.com/nats-io/nkeys" 28 | ) 29 | 30 | // ClaimType is used to indicate the type of JWT being stored in a Claim 31 | type ClaimType string 32 | 33 | const ( 34 | // OperatorClaim is the type of an operator JWT 35 | OperatorClaim = "operator" 36 | // AccountClaim is the type of an Account JWT 37 | AccountClaim = "account" 38 | // UserClaim is the type of an user JWT 39 | UserClaim = "user" 40 | // ActivationClaim is the type of an activation JWT 41 | ActivationClaim = "activation" 42 | // AuthorizationRequestClaim is the type of an auth request claim JWT 43 | AuthorizationRequestClaim = "authorization_request" 44 | // AuthorizationResponseClaim is the response for an auth request 45 | AuthorizationResponseClaim = "authorization_response" 46 | // GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim 47 | GenericClaim = "generic" 48 | ) 49 | 50 | func IsGenericClaimType(s string) bool { 51 | switch s { 52 | case OperatorClaim: 53 | fallthrough 54 | case AccountClaim: 55 | fallthrough 56 | case UserClaim: 57 | fallthrough 58 | case AuthorizationRequestClaim: 59 | fallthrough 60 | case AuthorizationResponseClaim: 61 | fallthrough 62 | case ActivationClaim: 63 | return false 64 | case GenericClaim: 65 | return true 66 | default: 67 | return true 68 | } 69 | } 70 | 71 | // SignFn is used in an external sign environment. The function should be 72 | // able to locate the private key for the specified pub key specified and sign the 73 | // specified data returning the signature as generated. 74 | type SignFn func(pub string, data []byte) ([]byte, error) 75 | 76 | // Claims is a JWT claims 77 | type Claims interface { 78 | Claims() *ClaimsData 79 | Encode(kp nkeys.KeyPair) (string, error) 80 | EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) 81 | ExpectedPrefixes() []nkeys.PrefixByte 82 | Payload() interface{} 83 | String() string 84 | Validate(vr *ValidationResults) 85 | ClaimType() ClaimType 86 | 87 | verify(payload string, sig []byte) bool 88 | updateVersion() 89 | } 90 | 91 | type GenericFields struct { 92 | Tags TagList `json:"tags,omitempty"` 93 | Type ClaimType `json:"type,omitempty"` 94 | Version int `json:"version,omitempty"` 95 | } 96 | 97 | // ClaimsData is the base struct for all claims 98 | type ClaimsData struct { 99 | Audience string `json:"aud,omitempty"` 100 | Expires int64 `json:"exp,omitempty"` 101 | ID string `json:"jti,omitempty"` 102 | IssuedAt int64 `json:"iat,omitempty"` 103 | Issuer string `json:"iss,omitempty"` 104 | Name string `json:"name,omitempty"` 105 | NotBefore int64 `json:"nbf,omitempty"` 106 | Subject string `json:"sub,omitempty"` 107 | } 108 | 109 | // Prefix holds the prefix byte for an NKey 110 | type Prefix struct { 111 | nkeys.PrefixByte 112 | } 113 | 114 | func encodeToString(d []byte) string { 115 | return base64.RawURLEncoding.EncodeToString(d) 116 | } 117 | 118 | func decodeString(s string) ([]byte, error) { 119 | return base64.RawURLEncoding.DecodeString(s) 120 | } 121 | 122 | func serialize(v interface{}) (string, error) { 123 | j, err := json.Marshal(v) 124 | if err != nil { 125 | return "", err 126 | } 127 | return encodeToString(j), nil 128 | } 129 | 130 | func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims, fn SignFn) (string, error) { 131 | if header == nil { 132 | return "", errors.New("header is required") 133 | } 134 | 135 | if kp == nil { 136 | return "", errors.New("keypair is required") 137 | } 138 | 139 | if c != claim.Claims() { 140 | return "", errors.New("claim and claim data do not match") 141 | } 142 | 143 | if c.Subject == "" { 144 | return "", errors.New("subject is not set") 145 | } 146 | 147 | h, err := serialize(header) 148 | if err != nil { 149 | return "", err 150 | } 151 | 152 | issuerBytes, err := kp.PublicKey() 153 | if err != nil { 154 | return "", err 155 | } 156 | 157 | prefixes := claim.ExpectedPrefixes() 158 | if prefixes != nil { 159 | ok := false 160 | for _, p := range prefixes { 161 | switch p { 162 | case nkeys.PrefixByteAccount: 163 | if nkeys.IsValidPublicAccountKey(issuerBytes) { 164 | ok = true 165 | } 166 | case nkeys.PrefixByteOperator: 167 | if nkeys.IsValidPublicOperatorKey(issuerBytes) { 168 | ok = true 169 | } 170 | case nkeys.PrefixByteServer: 171 | if nkeys.IsValidPublicServerKey(issuerBytes) { 172 | ok = true 173 | } 174 | case nkeys.PrefixByteCluster: 175 | if nkeys.IsValidPublicClusterKey(issuerBytes) { 176 | ok = true 177 | } 178 | case nkeys.PrefixByteUser: 179 | if nkeys.IsValidPublicUserKey(issuerBytes) { 180 | ok = true 181 | } 182 | } 183 | } 184 | if !ok { 185 | return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes) 186 | } 187 | } 188 | 189 | c.Issuer = issuerBytes 190 | c.IssuedAt = time.Now().UTC().Unix() 191 | c.ID = "" // to create a repeatable hash 192 | c.ID, err = c.hash() 193 | if err != nil { 194 | return "", err 195 | } 196 | 197 | claim.updateVersion() 198 | 199 | payload, err := serialize(claim) 200 | if err != nil { 201 | return "", err 202 | } 203 | 204 | toSign := fmt.Sprintf("%s.%s", h, payload) 205 | eSig := "" 206 | if header.Algorithm == AlgorithmNkeyOld { 207 | return "", errors.New(AlgorithmNkeyOld + " not supported to write jwtV2") 208 | } else if header.Algorithm == AlgorithmNkey { 209 | var sig []byte 210 | if fn != nil { 211 | pk, err := kp.PublicKey() 212 | if err != nil { 213 | return "", err 214 | } 215 | sig, err = fn(pk, []byte(toSign)) 216 | if err != nil { 217 | return "", err 218 | } 219 | } else { 220 | sig, err = kp.Sign([]byte(toSign)) 221 | if err != nil { 222 | return "", err 223 | } 224 | } 225 | eSig = encodeToString(sig) 226 | } else { 227 | return "", errors.New(header.Algorithm + " not supported to write jwtV2") 228 | } 229 | // hash need no padding 230 | return fmt.Sprintf("%s.%s", toSign, eSig), nil 231 | } 232 | 233 | func (c *ClaimsData) hash() (string, error) { 234 | j, err := json.Marshal(c) 235 | if err != nil { 236 | return "", err 237 | } 238 | h := sha512.New512_256() 239 | h.Write(j) 240 | return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil 241 | } 242 | 243 | // Encode encodes a claim into a JWT token. The claim is signed with the 244 | // provided nkey's private key 245 | func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims, fn SignFn) (string, error) { 246 | return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload, fn) 247 | } 248 | 249 | // Returns a JSON representation of the claim 250 | func (c *ClaimsData) String(claim interface{}) string { 251 | j, err := json.MarshalIndent(claim, "", " ") 252 | if err != nil { 253 | return "" 254 | } 255 | return string(j) 256 | } 257 | 258 | func parseClaims(s string, target Claims) error { 259 | h, err := decodeString(s) 260 | if err != nil { 261 | return err 262 | } 263 | return json.Unmarshal(h, &target) 264 | } 265 | 266 | // Verify verifies that the encoded payload was signed by the 267 | // provided public key. Verify is called automatically with 268 | // the claims portion of the token and the public key in the claim. 269 | // Client code need to insure that the public key in the 270 | // claim is trusted. 271 | func (c *ClaimsData) verify(payload string, sig []byte) bool { 272 | // decode the public key 273 | kp, err := nkeys.FromPublicKey(c.Issuer) 274 | if err != nil { 275 | return false 276 | } 277 | if err := kp.Verify([]byte(payload), sig); err != nil { 278 | return false 279 | } 280 | return true 281 | } 282 | 283 | // Validate checks a claim to make sure it is valid. Validity checks 284 | // include expiration and not before constraints. 285 | func (c *ClaimsData) Validate(vr *ValidationResults) { 286 | now := time.Now().UTC().Unix() 287 | if c.Expires > 0 && now > c.Expires { 288 | vr.AddTimeCheck("claim is expired") 289 | } 290 | 291 | if c.NotBefore > 0 && c.NotBefore > now { 292 | vr.AddTimeCheck("claim is not yet valid") 293 | } 294 | } 295 | 296 | // IsSelfSigned returns true if the claims issuer is the subject 297 | func (c *ClaimsData) IsSelfSigned() bool { 298 | return c.Issuer == c.Subject 299 | } 300 | -------------------------------------------------------------------------------- /v2/creds_utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "regexp" 23 | "strings" 24 | "time" 25 | 26 | "github.com/nats-io/nkeys" 27 | ) 28 | 29 | // DecorateJWT returns a decorated JWT that describes the kind of JWT 30 | func DecorateJWT(jwtString string) ([]byte, error) { 31 | gc, err := Decode(jwtString) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return formatJwt(string(gc.ClaimType()), jwtString) 36 | } 37 | 38 | func formatJwt(kind string, jwtString string) ([]byte, error) { 39 | templ := `-----BEGIN NATS %s JWT----- 40 | %s 41 | ------END NATS %s JWT------ 42 | 43 | ` 44 | w := bytes.NewBuffer(nil) 45 | kind = strings.ToUpper(kind) 46 | _, err := fmt.Fprintf(w, templ, kind, jwtString, kind) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return w.Bytes(), nil 51 | } 52 | 53 | // DecorateSeed takes a seed and returns a string that wraps 54 | // the seed in the form: 55 | // 56 | // ************************* IMPORTANT ************************* 57 | // NKEY Seed printed below can be used sign and prove identity. 58 | // NKEYs are sensitive and should be treated as secrets. 59 | // 60 | // -----BEGIN USER NKEY SEED----- 61 | // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM 62 | // ------END USER NKEY SEED------ 63 | func DecorateSeed(seed []byte) ([]byte, error) { 64 | w := bytes.NewBuffer(nil) 65 | ts := bytes.TrimSpace(seed) 66 | pre := string(ts[0:2]) 67 | kind := "" 68 | switch pre { 69 | case "SU": 70 | kind = "USER" 71 | case "SA": 72 | kind = "ACCOUNT" 73 | case "SO": 74 | kind = "OPERATOR" 75 | default: 76 | return nil, errors.New("seed is not an operator, account or user seed") 77 | } 78 | header := `************************* IMPORTANT ************************* 79 | NKEY Seed printed below can be used to sign and prove identity. 80 | NKEYs are sensitive and should be treated as secrets. 81 | 82 | -----BEGIN %s NKEY SEED----- 83 | ` 84 | _, err := fmt.Fprintf(w, header, kind) 85 | if err != nil { 86 | return nil, err 87 | } 88 | w.Write(ts) 89 | 90 | footer := ` 91 | ------END %s NKEY SEED------ 92 | 93 | ************************************************************* 94 | ` 95 | _, err = fmt.Fprintf(w, footer, kind) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return w.Bytes(), nil 100 | } 101 | 102 | var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`) 103 | 104 | // An user config file looks like this: 105 | // -----BEGIN NATS USER JWT----- 106 | // eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5... 107 | // ------END NATS USER JWT------ 108 | // 109 | // ************************* IMPORTANT ************************* 110 | // NKEY Seed printed below can be used sign and prove identity. 111 | // NKEYs are sensitive and should be treated as secrets. 112 | // 113 | // -----BEGIN USER NKEY SEED----- 114 | // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM 115 | // ------END USER NKEY SEED------ 116 | 117 | // FormatUserConfig returns a decorated file with a decorated JWT and decorated seed 118 | func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) { 119 | gc, err := Decode(jwtString) 120 | if err != nil { 121 | return nil, err 122 | } 123 | if gc.ClaimType() != UserClaim { 124 | return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.ClaimType())) 125 | } 126 | 127 | w := bytes.NewBuffer(nil) 128 | 129 | jd, err := formatJwt(string(gc.ClaimType()), jwtString) 130 | if err != nil { 131 | return nil, err 132 | } 133 | _, err = w.Write(jd) 134 | if err != nil { 135 | return nil, err 136 | } 137 | if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) { 138 | return nil, fmt.Errorf("nkey seed is not an user seed") 139 | } 140 | 141 | d, err := DecorateSeed(seed) 142 | if err != nil { 143 | return nil, err 144 | } 145 | _, err = w.Write(d) 146 | if err != nil { 147 | return nil, err 148 | } 149 | 150 | return w.Bytes(), nil 151 | } 152 | 153 | // ParseDecoratedJWT takes a creds file and returns the JWT portion. 154 | func ParseDecoratedJWT(contents []byte) (string, error) { 155 | items := userConfigRE.FindAllSubmatch(contents, -1) 156 | if len(items) == 0 { 157 | return string(contents), nil 158 | } 159 | // First result should be the user JWT. 160 | // We copy here so that if the file contained a seed file too we wipe appropriately. 161 | raw := items[0][1] 162 | tmp := make([]byte, len(raw)) 163 | copy(tmp, raw) 164 | return string(tmp), nil 165 | } 166 | 167 | // ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a 168 | // key pair from it. 169 | func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) { 170 | var seed []byte 171 | 172 | items := userConfigRE.FindAllSubmatch(contents, -1) 173 | if len(items) > 1 { 174 | seed = items[1][1] 175 | } else { 176 | lines := bytes.Split(contents, []byte("\n")) 177 | for _, line := range lines { 178 | if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) || 179 | bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) || 180 | bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) { 181 | seed = line 182 | break 183 | } 184 | } 185 | } 186 | if seed == nil { 187 | return nil, errors.New("no nkey seed found") 188 | } 189 | if !bytes.HasPrefix(seed, []byte("SO")) && 190 | !bytes.HasPrefix(seed, []byte("SA")) && 191 | !bytes.HasPrefix(seed, []byte("SU")) { 192 | return nil, errors.New("doesn't contain a seed nkey") 193 | } 194 | kp, err := nkeys.FromSeed(seed) 195 | if err != nil { 196 | return nil, err 197 | } 198 | return kp, nil 199 | } 200 | 201 | // ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a 202 | // key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys. 203 | func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) { 204 | nk, err := ParseDecoratedNKey(contents) 205 | if err != nil { 206 | return nil, err 207 | } 208 | seed, err := nk.Seed() 209 | if err != nil { 210 | return nil, err 211 | } 212 | if !bytes.HasPrefix(seed, []byte("SU")) { 213 | return nil, errors.New("doesn't contain an user seed nkey") 214 | } 215 | kp, err := nkeys.FromSeed(seed) 216 | if err != nil { 217 | return nil, err 218 | } 219 | return kp, nil 220 | } 221 | 222 | // 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. 223 | // 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). 224 | // The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey. 225 | // The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey. 226 | // The name, is an optional human-readable name. When absent, default to publicUserKey. 227 | // The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. 228 | // The tags, is an optional list of tags to be included in the JWT. 229 | // 230 | // Returns: 231 | // string, resulting jwt. 232 | // error, when issues arose. 233 | func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) { 234 | 235 | if !nkeys.IsValidPublicAccountKey(accountId) { 236 | return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String()) 237 | } 238 | 239 | if !nkeys.IsValidPublicUserKey(publicUserKey) { 240 | return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String()) 241 | } 242 | 243 | claim := NewUserClaims(publicUserKey) 244 | claim.SetScoped(true) 245 | 246 | if expirationDuration != 0 { 247 | claim.Expires = time.Now().Add(expirationDuration).UTC().Unix() 248 | } 249 | 250 | claim.IssuerAccount = accountId 251 | if name != "" { 252 | claim.Name = name 253 | } else { 254 | claim.Name = publicUserKey 255 | } 256 | 257 | claim.Subject = publicUserKey 258 | claim.Tags = tags 259 | 260 | encoded, err := claim.Encode(scopedSigningKey) 261 | if err != nil { 262 | return "", errors.New("err encoding claim " + err.Error()) 263 | } 264 | 265 | return encoded, nil 266 | } 267 | -------------------------------------------------------------------------------- /v2/decoder.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2022 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/nats-io/nkeys" 25 | ) 26 | 27 | const libVersion = 2 28 | 29 | type identifier struct { 30 | Type ClaimType `json:"type,omitempty"` 31 | GenericFields `json:"nats,omitempty"` 32 | } 33 | 34 | func (i *identifier) Kind() ClaimType { 35 | if i.Type != "" { 36 | return i.Type 37 | } 38 | return i.GenericFields.Type 39 | } 40 | 41 | func (i *identifier) Version() int { 42 | if i.Type != "" { 43 | return 1 44 | } 45 | return i.GenericFields.Version 46 | } 47 | 48 | type v1ClaimsDataDeletedFields struct { 49 | Tags TagList `json:"tags,omitempty"` 50 | Type ClaimType `json:"type,omitempty"` 51 | IssuerAccount string `json:"issuer_account,omitempty"` 52 | } 53 | 54 | // Decode takes a JWT string decodes it and validates it 55 | // and return the embedded Claims. If the token header 56 | // doesn't match the expected algorithm, or the claim is 57 | // not valid or verification fails an error is returned. 58 | func Decode(token string) (Claims, error) { 59 | // must have 3 chunks 60 | chunks := strings.Split(token, ".") 61 | if len(chunks) != 3 { 62 | return nil, errors.New("expected 3 chunks") 63 | } 64 | 65 | // header 66 | if _, err := parseHeaders(chunks[0]); err != nil { 67 | return nil, err 68 | } 69 | // claim 70 | data, err := decodeString(chunks[1]) 71 | if err != nil { 72 | return nil, err 73 | } 74 | ver, claim, err := loadClaims(data) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | // sig 80 | sig, err := decodeString(chunks[2]) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | if ver <= 1 { 86 | if !claim.verify(chunks[1], sig) { 87 | return nil, errors.New("claim failed V1 signature verification") 88 | } 89 | } else { 90 | if !claim.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) { 91 | return nil, errors.New("claim failed V2 signature verification") 92 | } 93 | } 94 | 95 | prefixes := claim.ExpectedPrefixes() 96 | if prefixes != nil { 97 | ok := false 98 | issuer := claim.Claims().Issuer 99 | for _, p := range prefixes { 100 | switch p { 101 | case nkeys.PrefixByteAccount: 102 | if nkeys.IsValidPublicAccountKey(issuer) { 103 | ok = true 104 | } 105 | case nkeys.PrefixByteOperator: 106 | if nkeys.IsValidPublicOperatorKey(issuer) { 107 | ok = true 108 | } 109 | case nkeys.PrefixByteUser: 110 | if nkeys.IsValidPublicUserKey(issuer) { 111 | ok = true 112 | } 113 | case nkeys.PrefixByteServer: 114 | if nkeys.IsValidPublicServerKey(issuer) { 115 | ok = true 116 | } 117 | } 118 | } 119 | if !ok { 120 | return nil, fmt.Errorf("unable to validate expected prefixes - %v", prefixes) 121 | } 122 | } 123 | return claim, nil 124 | } 125 | 126 | func loadClaims(data []byte) (int, Claims, error) { 127 | var id identifier 128 | if err := json.Unmarshal(data, &id); err != nil { 129 | return -1, nil, err 130 | } 131 | 132 | if id.Version() > libVersion { 133 | return -1, nil, errors.New("JWT was generated by a newer version ") 134 | } 135 | 136 | var claim Claims 137 | var err error 138 | switch id.Kind() { 139 | case OperatorClaim: 140 | claim, err = loadOperator(data, id.Version()) 141 | case AccountClaim: 142 | claim, err = loadAccount(data, id.Version()) 143 | case UserClaim: 144 | claim, err = loadUser(data, id.Version()) 145 | case ActivationClaim: 146 | claim, err = loadActivation(data, id.Version()) 147 | case AuthorizationRequestClaim: 148 | claim, err = loadAuthorizationRequest(data, id.Version()) 149 | case AuthorizationResponseClaim: 150 | claim, err = loadAuthorizationResponse(data, id.Version()) 151 | case "cluster": 152 | return -1, nil, errors.New("ClusterClaims are not supported") 153 | case "server": 154 | return -1, nil, errors.New("ServerClaims are not supported") 155 | default: 156 | var gc GenericClaims 157 | if err := json.Unmarshal(data, &gc); err != nil { 158 | return -1, nil, err 159 | } 160 | return -1, &gc, nil 161 | } 162 | 163 | return id.Version(), claim, err 164 | } 165 | -------------------------------------------------------------------------------- /v2/decoder_account.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | ) 22 | 23 | type v1NatsAccount struct { 24 | Imports Imports `json:"imports,omitempty"` 25 | Exports Exports `json:"exports,omitempty"` 26 | Limits struct { 27 | NatsLimits 28 | AccountLimits 29 | } `json:"limits,omitempty"` 30 | SigningKeys StringList `json:"signing_keys,omitempty"` 31 | Revocations RevocationList `json:"revocations,omitempty"` 32 | } 33 | 34 | func loadAccount(data []byte, version int) (*AccountClaims, error) { 35 | switch version { 36 | case 1: 37 | var v1a v1AccountClaims 38 | if err := json.Unmarshal(data, &v1a); err != nil { 39 | return nil, err 40 | } 41 | return v1a.Migrate() 42 | case 2: 43 | var v2a AccountClaims 44 | v2a.SigningKeys = make(SigningKeys) 45 | if err := json.Unmarshal(data, &v2a); err != nil { 46 | return nil, err 47 | } 48 | if len(v2a.Limits.JetStreamTieredLimits) > 0 { 49 | v2a.Limits.JetStreamLimits = JetStreamLimits{} 50 | } 51 | return &v2a, nil 52 | default: 53 | return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) 54 | } 55 | } 56 | 57 | type v1AccountClaims struct { 58 | ClaimsData 59 | v1ClaimsDataDeletedFields 60 | v1NatsAccount `json:"nats,omitempty"` 61 | } 62 | 63 | func (oa v1AccountClaims) Migrate() (*AccountClaims, error) { 64 | return oa.migrateV1() 65 | } 66 | 67 | func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) { 68 | var a AccountClaims 69 | // copy the base claim 70 | a.ClaimsData = oa.ClaimsData 71 | // move the moved fields 72 | a.Account.Type = oa.v1ClaimsDataDeletedFields.Type 73 | a.Account.Tags = oa.v1ClaimsDataDeletedFields.Tags 74 | // copy the account data 75 | a.Account.Imports = oa.v1NatsAccount.Imports 76 | a.Account.Exports = oa.v1NatsAccount.Exports 77 | a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits 78 | a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits 79 | a.Account.Limits.JetStreamLimits = JetStreamLimits{} 80 | a.Account.SigningKeys = make(SigningKeys) 81 | for _, v := range oa.SigningKeys { 82 | a.Account.SigningKeys.Add(v) 83 | } 84 | a.Account.Revocations = oa.v1NatsAccount.Revocations 85 | a.Version = 1 86 | return &a, nil 87 | } 88 | -------------------------------------------------------------------------------- /v2/decoder_activation.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package jwt 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | ) 21 | 22 | // Migration adds GenericFields 23 | type v1NatsActivation struct { 24 | ImportSubject Subject `json:"subject,omitempty"` 25 | ImportType ExportType `json:"type,omitempty"` 26 | // Limit values deprecated inv v2 27 | Max int64 `json:"max,omitempty"` 28 | Payload int64 `json:"payload,omitempty"` 29 | Src string `json:"src,omitempty"` 30 | Times []TimeRange `json:"times,omitempty"` 31 | } 32 | 33 | type v1ActivationClaims struct { 34 | ClaimsData 35 | v1ClaimsDataDeletedFields 36 | v1NatsActivation `json:"nats,omitempty"` 37 | } 38 | 39 | func loadActivation(data []byte, version int) (*ActivationClaims, error) { 40 | switch version { 41 | case 1: 42 | var v1a v1ActivationClaims 43 | v1a.Max = NoLimit 44 | v1a.Payload = NoLimit 45 | if err := json.Unmarshal(data, &v1a); err != nil { 46 | return nil, err 47 | } 48 | return v1a.Migrate() 49 | case 2: 50 | var v2a ActivationClaims 51 | if err := json.Unmarshal(data, &v2a); err != nil { 52 | return nil, err 53 | } 54 | return &v2a, nil 55 | default: 56 | return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) 57 | } 58 | } 59 | 60 | func (oa v1ActivationClaims) Migrate() (*ActivationClaims, error) { 61 | return oa.migrateV1() 62 | } 63 | 64 | func (oa v1ActivationClaims) migrateV1() (*ActivationClaims, error) { 65 | var a ActivationClaims 66 | // copy the base claim 67 | a.ClaimsData = oa.ClaimsData 68 | // move the moved fields 69 | a.Activation.Type = oa.v1ClaimsDataDeletedFields.Type 70 | a.Activation.Tags = oa.v1ClaimsDataDeletedFields.Tags 71 | a.Activation.IssuerAccount = oa.v1ClaimsDataDeletedFields.IssuerAccount 72 | // copy the activation data 73 | a.ImportSubject = oa.ImportSubject 74 | a.ImportType = oa.ImportType 75 | a.Version = 1 76 | return &a, nil 77 | } 78 | -------------------------------------------------------------------------------- /v2/decoder_authorization.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | ) 21 | 22 | func loadAuthorizationRequest(data []byte, version int) (*AuthorizationRequestClaims, error) { 23 | var ac AuthorizationRequestClaims 24 | if err := json.Unmarshal(data, &ac); err != nil { 25 | return nil, err 26 | } 27 | return &ac, nil 28 | } 29 | 30 | func loadAuthorizationResponse(data []byte, version int) (*AuthorizationResponseClaims, error) { 31 | var ac AuthorizationResponseClaims 32 | if err := json.Unmarshal(data, &ac); err != nil { 33 | return nil, err 34 | } 35 | return &ac, nil 36 | } 37 | -------------------------------------------------------------------------------- /v2/decoder_operator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | ) 22 | 23 | type v1NatsOperator struct { 24 | SigningKeys StringList `json:"signing_keys,omitempty"` 25 | AccountServerURL string `json:"account_server_url,omitempty"` 26 | OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` 27 | SystemAccount string `json:"system_account,omitempty"` 28 | } 29 | 30 | func loadOperator(data []byte, version int) (*OperatorClaims, error) { 31 | switch version { 32 | case 1: 33 | var v1a v1OperatorClaims 34 | if err := json.Unmarshal(data, &v1a); err != nil { 35 | return nil, err 36 | } 37 | return v1a.Migrate() 38 | case 2: 39 | var v2a OperatorClaims 40 | if err := json.Unmarshal(data, &v2a); err != nil { 41 | return nil, err 42 | } 43 | return &v2a, nil 44 | default: 45 | return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) 46 | } 47 | } 48 | 49 | type v1OperatorClaims struct { 50 | ClaimsData 51 | v1ClaimsDataDeletedFields 52 | v1NatsOperator `json:"nats,omitempty"` 53 | } 54 | 55 | func (oa v1OperatorClaims) Migrate() (*OperatorClaims, error) { 56 | return oa.migrateV1() 57 | } 58 | 59 | func (oa v1OperatorClaims) migrateV1() (*OperatorClaims, error) { 60 | var a OperatorClaims 61 | // copy the base claim 62 | a.ClaimsData = oa.ClaimsData 63 | // move the moved fields 64 | a.Operator.Type = oa.v1ClaimsDataDeletedFields.Type 65 | a.Operator.Tags = oa.v1ClaimsDataDeletedFields.Tags 66 | // copy the account data 67 | a.Operator.SigningKeys = oa.v1NatsOperator.SigningKeys 68 | a.Operator.AccountServerURL = oa.v1NatsOperator.AccountServerURL 69 | a.Operator.OperatorServiceURLs = oa.v1NatsOperator.OperatorServiceURLs 70 | a.Operator.SystemAccount = oa.v1NatsOperator.SystemAccount 71 | a.Version = 1 72 | return &a, nil 73 | } 74 | -------------------------------------------------------------------------------- /v2/decoder_user.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | ) 22 | 23 | type v1User struct { 24 | Permissions 25 | Limits 26 | BearerToken bool `json:"bearer_token,omitempty"` 27 | // Limit values deprecated inv v2 28 | Max int64 `json:"max,omitempty"` 29 | } 30 | 31 | type v1UserClaimsDataDeletedFields struct { 32 | v1ClaimsDataDeletedFields 33 | IssuerAccount string `json:"issuer_account,omitempty"` 34 | } 35 | 36 | type v1UserClaims struct { 37 | ClaimsData 38 | v1UserClaimsDataDeletedFields 39 | v1User `json:"nats,omitempty"` 40 | } 41 | 42 | func loadUser(data []byte, version int) (*UserClaims, error) { 43 | switch version { 44 | case 1: 45 | var v1a v1UserClaims 46 | v1a.Limits = Limits{NatsLimits: NatsLimits{NoLimit, NoLimit, NoLimit}} 47 | v1a.Max = NoLimit 48 | if err := json.Unmarshal(data, &v1a); err != nil { 49 | return nil, err 50 | } 51 | return v1a.Migrate() 52 | case 2: 53 | var v2a UserClaims 54 | if err := json.Unmarshal(data, &v2a); err != nil { 55 | return nil, err 56 | } 57 | return &v2a, nil 58 | default: 59 | return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version) 60 | } 61 | } 62 | 63 | func (oa v1UserClaims) Migrate() (*UserClaims, error) { 64 | return oa.migrateV1() 65 | } 66 | 67 | func (oa v1UserClaims) migrateV1() (*UserClaims, error) { 68 | var u UserClaims 69 | // copy the base claim 70 | u.ClaimsData = oa.ClaimsData 71 | // move the moved fields 72 | u.User.Type = oa.v1ClaimsDataDeletedFields.Type 73 | u.User.Tags = oa.v1ClaimsDataDeletedFields.Tags 74 | u.User.IssuerAccount = oa.IssuerAccount 75 | // copy the user data 76 | u.User.Permissions = oa.v1User.Permissions 77 | u.User.Limits = oa.v1User.Limits 78 | u.User.BearerToken = oa.v1User.BearerToken 79 | u.Version = 1 80 | return &u, nil 81 | } 82 | -------------------------------------------------------------------------------- /v2/example_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | jwt "github.com/nats-io/jwt/v2/v1compat" 10 | "github.com/nats-io/nkeys" 11 | ) 12 | 13 | func TestExample(t *testing.T) { 14 | // create an operator key pair (private key) 15 | okp, err := nkeys.CreateOperator() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | // extract the public key 20 | opk, err := okp.PublicKey() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | // create an operator claim using the public key for the identifier 26 | oc := jwt.NewOperatorClaims(opk) 27 | oc.Name = "O" 28 | // add an operator signing key to sign accounts 29 | oskp, err := nkeys.CreateOperator() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | // get the public key for the signing key 34 | ospk, err := oskp.PublicKey() 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | // add the signing key to the operator - this makes any account 39 | // issued by the signing key to be valid for the operator 40 | oc.SigningKeys.Add(ospk) 41 | 42 | // self-sign the operator JWT - the operator trusts itself 43 | operatorJWT, err := oc.Encode(okp) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | // create an account keypair 49 | akp, err := nkeys.CreateAccount() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | // extract the public key for the account 54 | apk, err := akp.PublicKey() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | // create the claim for the account using the public key of the account 59 | ac := jwt.NewAccountClaims(apk) 60 | ac.Name = "A" 61 | // create a signing key that we can use for issuing users 62 | askp, err := nkeys.CreateAccount() 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | // extract the public key 67 | aspk, err := askp.PublicKey() 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | // add the signing key (public) to the account 72 | ac.SigningKeys.Add(aspk) 73 | 74 | // now we could encode an issue the account using the operator 75 | // key that we generated above, but this will illustrate that 76 | // the account could be self-signed, and given to the operator 77 | // who can then re-sign it 78 | accountJWT, err := ac.Encode(akp) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | // the operator would decode the provided token, if the token 84 | // is not self-signed or signed by an operator or tampered with 85 | // the decoding would fail 86 | ac, err = jwt.DecodeAccountClaims(accountJWT) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | // here the operator is going to use its private signing key to 91 | // re-issue the account 92 | accountJWT, err = ac.Encode(oskp) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | // now back to the account, the account can issue users 98 | // need not be known to the operator - the users are trusted 99 | // because they will be signed by the account. The server will 100 | // look up the account get a list of keys the account has and 101 | // verify that the user was issued by one of those keys 102 | ukp, err := nkeys.CreateUser() 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | upk, err := ukp.PublicKey() 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | uc := jwt.NewUserClaims(upk) 111 | // since the jwt will be issued by a signing key, the issuer account 112 | // must be set to the public ID of the account 113 | uc.IssuerAccount = apk 114 | userJwt, err := uc.Encode(askp) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | // the seed is a version of the keypair that is stored as text 119 | useed, err := ukp.Seed() 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | // generate a creds formatted file that can be used by a NATS client 124 | creds, err := jwt.FormatUserConfig(userJwt, useed) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | // now we are going to put it together into something that can be run 130 | // we create a directory to store the server configuration, the creds 131 | // file and a small go program that uses the creds file 132 | dir, err := os.MkdirTemp(os.TempDir(), "jwt_example") 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | // print where we generated the file 137 | t.Logf("generated example %s", dir) 138 | t.Log("to run this example:") 139 | t.Logf("> cd %s", dir) 140 | t.Log("> go mod init example") 141 | t.Log("> go mod tidy") 142 | t.Logf("> nats-server -c %s/resolver.conf &", dir) 143 | t.Log("> go run main.go") 144 | 145 | // we are generating a memory resolver server configuration 146 | // it lists the operator and all account jwts the server should 147 | // know about 148 | resolver := fmt.Sprintf(`operator: %s 149 | 150 | resolver: MEMORY 151 | resolver_preload: { 152 | %s: %s 153 | } 154 | `, operatorJWT, apk, accountJWT) 155 | if err := os.WriteFile(path.Join(dir, "resolver.conf"), 156 | []byte(resolver), 0644); err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | // store the creds 161 | credsPath := path.Join(dir, "u.creds") 162 | if err := os.WriteFile(credsPath, creds, 0644); err != nil { 163 | t.Fatal(err) 164 | } 165 | 166 | // here we generate as small go program that connects using the creds file 167 | // subscribes, and publishes a message 168 | connect := fmt.Sprintf(` 169 | package main 170 | 171 | import ( 172 | "fmt" 173 | "sync" 174 | 175 | "github.com/nats-io/nats.go" 176 | ) 177 | 178 | func main() { 179 | var wg sync.WaitGroup 180 | wg.Add(1) 181 | nc, err := nats.Connect(nats.DefaultURL, nats.UserCredentials(%q)) 182 | if err != nil { 183 | panic(err) 184 | } 185 | nc.Subscribe("hello.world", func(m *nats.Msg) { 186 | fmt.Println(m.Subject) 187 | wg.Done() 188 | }) 189 | nc.Publish("hello.world", []byte("hello")) 190 | nc.Flush() 191 | wg.Wait() 192 | nc.Close() 193 | } 194 | 195 | `, credsPath) 196 | if err := os.WriteFile(path.Join(dir, "main.go"), []byte(connect), 0644); err != nil { 197 | t.Fatal(err) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /v2/genericlaims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "strings" 22 | 23 | "github.com/nats-io/nkeys" 24 | ) 25 | 26 | // GenericClaims can be used to read a JWT as a map for any non-generic fields 27 | type GenericClaims struct { 28 | ClaimsData 29 | Data map[string]interface{} `json:"nats,omitempty"` 30 | } 31 | 32 | // NewGenericClaims creates a map-based Claims 33 | func NewGenericClaims(subject string) *GenericClaims { 34 | if subject == "" { 35 | return nil 36 | } 37 | c := GenericClaims{} 38 | c.Subject = subject 39 | c.Data = make(map[string]interface{}) 40 | return &c 41 | } 42 | 43 | // DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map 44 | func DecodeGeneric(token string) (*GenericClaims, error) { 45 | // must have 3 chunks 46 | chunks := strings.Split(token, ".") 47 | if len(chunks) != 3 { 48 | return nil, errors.New("expected 3 chunks") 49 | } 50 | 51 | // header 52 | header, err := parseHeaders(chunks[0]) 53 | if err != nil { 54 | return nil, err 55 | } 56 | // claim 57 | data, err := decodeString(chunks[1]) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | gc := struct { 63 | GenericClaims 64 | GenericFields 65 | }{} 66 | if err := json.Unmarshal(data, &gc); err != nil { 67 | return nil, err 68 | } 69 | 70 | // sig 71 | sig, err := decodeString(chunks[2]) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | if header.Algorithm == AlgorithmNkeyOld { 77 | if !gc.verify(chunks[1], sig) { 78 | return nil, errors.New("claim failed V1 signature verification") 79 | } 80 | if tp := gc.GenericFields.Type; tp != "" { 81 | // the conversion needs to be from a string because 82 | // on custom types the type is not going to be one of 83 | // the constants 84 | gc.GenericClaims.Data["type"] = string(tp) 85 | } 86 | if tp := gc.GenericFields.Tags; len(tp) != 0 { 87 | gc.GenericClaims.Data["tags"] = tp 88 | } 89 | 90 | } else { 91 | if !gc.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) { 92 | return nil, errors.New("claim failed V2 signature verification") 93 | } 94 | } 95 | return &gc.GenericClaims, nil 96 | } 97 | 98 | // Claims returns the standard part of the generic claim 99 | func (gc *GenericClaims) Claims() *ClaimsData { 100 | return &gc.ClaimsData 101 | } 102 | 103 | // Payload returns the custom part of the claims data 104 | func (gc *GenericClaims) Payload() interface{} { 105 | return &gc.Data 106 | } 107 | 108 | // Encode takes a generic claims and creates a JWT string 109 | func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) { 110 | return gc.ClaimsData.encode(pair, gc, nil) 111 | } 112 | 113 | func (gc *GenericClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { 114 | return gc.ClaimsData.encode(pair, gc, fn) 115 | } 116 | 117 | // Validate checks the generic part of the claims data 118 | func (gc *GenericClaims) Validate(vr *ValidationResults) { 119 | gc.ClaimsData.Validate(vr) 120 | } 121 | 122 | func (gc *GenericClaims) String() string { 123 | return gc.ClaimsData.String(gc) 124 | } 125 | 126 | // ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all 127 | func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte { 128 | return nil 129 | } 130 | 131 | func (gc *GenericClaims) ClaimType() ClaimType { 132 | v, ok := gc.Data["type"] 133 | if !ok { 134 | v, ok = gc.Data["nats"] 135 | if ok { 136 | m, ok := v.(map[string]interface{}) 137 | if ok { 138 | v = m["type"] 139 | } 140 | } 141 | } 142 | 143 | switch ct := v.(type) { 144 | case string: 145 | if IsGenericClaimType(ct) { 146 | return GenericClaim 147 | } 148 | return ClaimType(ct) 149 | case ClaimType: 150 | return ct 151 | default: 152 | return "" 153 | } 154 | } 155 | 156 | func (gc *GenericClaims) updateVersion() { 157 | if gc.Data != nil { 158 | // store as float as that is what decoding with json does too 159 | gc.Data["version"] = float64(libVersion) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nats-io/jwt/v2 2 | 3 | go 1.23.0 4 | 5 | require github.com/nats-io/nkeys v0.4.11 6 | 7 | retract ( 8 | v2.7.1 // contains retractions only 9 | v2.7.0 // includes case insensitive changes to tags that break jetstream placement 10 | ) 11 | 12 | require ( 13 | golang.org/x/crypto v0.37.0 // indirect 14 | golang.org/x/sys v0.32.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= 2 | github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= 3 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 4 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 5 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 6 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 7 | -------------------------------------------------------------------------------- /v2/header.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2019 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | // Version is semantic version. 26 | Version = "2.4.0" 27 | 28 | // TokenTypeJwt is the JWT token type supported JWT tokens 29 | // encoded and decoded by this library 30 | // from RFC7519 5.1 "typ": 31 | // it is RECOMMENDED that "JWT" always be spelled using uppercase characters for compatibility 32 | TokenTypeJwt = "JWT" 33 | 34 | // AlgorithmNkey is the algorithm supported by JWT tokens 35 | // encoded and decoded by this library 36 | AlgorithmNkeyOld = "ed25519" 37 | AlgorithmNkey = AlgorithmNkeyOld + "-nkey" 38 | ) 39 | 40 | // Header is a JWT Jose Header 41 | type Header struct { 42 | Type string `json:"typ"` 43 | Algorithm string `json:"alg"` 44 | } 45 | 46 | // Parses a header JWT token 47 | func parseHeaders(s string) (*Header, error) { 48 | h, err := decodeString(s) 49 | if err != nil { 50 | return nil, err 51 | } 52 | header := Header{} 53 | if err := json.Unmarshal(h, &header); err != nil { 54 | return nil, err 55 | } 56 | 57 | if err := header.Valid(); err != nil { 58 | return nil, err 59 | } 60 | return &header, nil 61 | } 62 | 63 | // Valid validates the Header. It returns nil if the Header is 64 | // a JWT header, and the algorithm used is the NKEY algorithm. 65 | func (h *Header) Valid() error { 66 | if TokenTypeJwt != strings.ToUpper(h.Type) { 67 | return fmt.Errorf("not supported type %q", h.Type) 68 | } 69 | 70 | alg := strings.ToLower(h.Algorithm) 71 | if !strings.HasPrefix(alg, AlgorithmNkeyOld) { 72 | return fmt.Errorf("unexpected %q algorithm", h.Algorithm) 73 | } 74 | if AlgorithmNkeyOld != alg && AlgorithmNkey != alg { 75 | return fmt.Errorf("unexpected %q algorithm", h.Algorithm) 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /v2/imports.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | // Import describes a mapping from another account into this one 19 | type Import struct { 20 | Name string `json:"name,omitempty"` 21 | // Subject field in an import is always from the perspective of the 22 | // initial publisher - in the case of a stream it is the account owning 23 | // the stream (the exporter), and in the case of a service it is the 24 | // account making the request (the importer). 25 | Subject Subject `json:"subject,omitempty"` 26 | Account string `json:"account,omitempty"` 27 | Token string `json:"token,omitempty"` 28 | // Deprecated: use LocalSubject instead 29 | // To field in an import is always from the perspective of the subscriber 30 | // in the case of a stream it is the client of the stream (the importer), 31 | // from the perspective of a service, it is the subscription waiting for 32 | // requests (the exporter). If the field is empty, it will default to the 33 | // value in the Subject field. 34 | To Subject `json:"to,omitempty"` 35 | // Local subject used to subscribe (for streams) and publish (for services) to. 36 | // This value only needs setting if you want to change the value of Subject. 37 | // If the value of Subject ends in > then LocalSubject needs to end in > as well. 38 | // LocalSubject can contain $ wildcard references where number references the nth wildcard in Subject. 39 | // The sum of wildcard reference and * tokens needs to match the number of * token in Subject. 40 | LocalSubject RenamingSubject `json:"local_subject,omitempty"` 41 | Type ExportType `json:"type,omitempty"` 42 | Share bool `json:"share,omitempty"` 43 | AllowTrace bool `json:"allow_trace,omitempty"` 44 | } 45 | 46 | // IsService returns true if the import is of type service 47 | func (i *Import) IsService() bool { 48 | return i.Type == Service 49 | } 50 | 51 | // IsStream returns true if the import is of type stream 52 | func (i *Import) IsStream() bool { 53 | return i.Type == Stream 54 | } 55 | 56 | // Returns the value of To without triggering the deprecation warning for a read 57 | func (i *Import) GetTo() string { 58 | return string(i.To) 59 | } 60 | 61 | // Validate checks if an import is valid for the wrapping account 62 | func (i *Import) Validate(actPubKey string, vr *ValidationResults) { 63 | if i == nil { 64 | vr.AddError("null import is not allowed") 65 | return 66 | } 67 | if !i.IsService() && !i.IsStream() { 68 | vr.AddError("invalid import type: %q", i.Type) 69 | } 70 | if i.IsService() && i.AllowTrace { 71 | vr.AddError("AllowTrace only valid for stream import") 72 | } 73 | 74 | if i.Account == "" { 75 | vr.AddError("account to import from is not specified") 76 | } 77 | 78 | if i.GetTo() != "" { 79 | vr.AddWarning("the field to has been deprecated (use LocalSubject instead)") 80 | } 81 | 82 | i.Subject.Validate(vr) 83 | if i.LocalSubject != "" { 84 | i.LocalSubject.Validate(i.Subject, vr) 85 | if i.To != "" { 86 | vr.AddError("Local Subject replaces To") 87 | } 88 | } 89 | 90 | if i.Share && !i.IsService() { 91 | vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject) 92 | } 93 | var act *ActivationClaims 94 | 95 | if i.Token != "" { 96 | var err error 97 | act, err = DecodeActivationClaims(i.Token) 98 | if err != nil { 99 | vr.AddError("import %q contains an invalid activation token", i.Subject) 100 | } 101 | } 102 | 103 | if act != nil { 104 | if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { 105 | vr.AddError("activation token doesn't match account for import %q", i.Subject) 106 | } 107 | if act.ClaimsData.Subject != actPubKey { 108 | vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) 109 | } 110 | if act.ImportType != i.Type { 111 | vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) 112 | } 113 | act.validateWithTimeChecks(vr, false) 114 | subj := i.Subject 115 | if i.IsService() && i.To != "" { 116 | subj = i.To 117 | } 118 | if !subj.IsContainedIn(act.ImportSubject) { 119 | vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) 120 | } 121 | } 122 | } 123 | 124 | // Imports is a list of import structs 125 | type Imports []*Import 126 | 127 | // Validate checks if an import is valid for the wrapping account 128 | func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) { 129 | toSet := make(map[Subject]struct{}, len(*i)) 130 | for _, v := range *i { 131 | if v == nil { 132 | vr.AddError("null import is not allowed") 133 | continue 134 | } 135 | if v.Type == Service { 136 | sub := v.To 137 | if sub == "" { 138 | sub = v.LocalSubject.ToSubject() 139 | } 140 | if sub == "" { 141 | sub = v.Subject 142 | } 143 | for k := range toSet { 144 | if sub.IsContainedIn(k) || k.IsContainedIn(sub) { 145 | vr.AddError("overlapping subject namespace for %q and %q", sub, k) 146 | } 147 | } 148 | if _, ok := toSet[sub]; ok { 149 | vr.AddError("overlapping subject namespace for %q", v.To) 150 | } 151 | toSet[sub] = struct{}{} 152 | } 153 | v.Validate(acctPubKey, vr) 154 | } 155 | } 156 | 157 | // Add is a simple way to add imports 158 | func (i *Imports) Add(a ...*Import) { 159 | *i = append(*i, a...) 160 | } 161 | 162 | func (i Imports) Len() int { 163 | return len(i) 164 | } 165 | 166 | func (i Imports) Swap(j, k int) { 167 | i[j], i[k] = i[k], i[j] 168 | } 169 | 170 | func (i Imports) Less(j, k int) bool { 171 | return i[j].Subject < i[k].Subject 172 | } 173 | -------------------------------------------------------------------------------- /v2/operator_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "net/url" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | // Operator specific claims 29 | type Operator struct { 30 | // Slice of other operator NKeys that can be used to sign on behalf of the main 31 | // operator identity. 32 | SigningKeys StringList `json:"signing_keys,omitempty"` 33 | // AccountServerURL is a partial URL like "https://host.domain.org:/jwt/v1" 34 | // tools will use the prefix and build queries by appending /accounts/ 35 | // or /operator to the path provided. Note this assumes that the account server 36 | // can handle requests in a nats-account-server compatible way. See 37 | // https://github.com/nats-io/nats-account-server. 38 | AccountServerURL string `json:"account_server_url,omitempty"` 39 | // A list of NATS urls (tls://host:port) where tools can connect to the server 40 | // using proper credentials. 41 | OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` 42 | // Identity of the system account 43 | SystemAccount string `json:"system_account,omitempty"` 44 | // Min Server version 45 | AssertServerVersion string `json:"assert_server_version,omitempty"` 46 | // Signing of subordinate objects will require signing keys 47 | StrictSigningKeyUsage bool `json:"strict_signing_key_usage,omitempty"` 48 | GenericFields 49 | } 50 | 51 | func ParseServerVersion(version string) (int, int, int, error) { 52 | if version == "" { 53 | return 0, 0, 0, nil 54 | } 55 | split := strings.Split(version, ".") 56 | if len(split) != 3 { 57 | return 0, 0, 0, fmt.Errorf("asserted server version must be of the form ..") 58 | } else if major, err := strconv.Atoi(split[0]); err != nil { 59 | return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[0]) 60 | } else if minor, err := strconv.Atoi(split[1]); err != nil { 61 | return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[1]) 62 | } else if update, err := strconv.Atoi(split[2]); err != nil { 63 | return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[2]) 64 | } else if major < 0 || minor < 0 || update < 0 { 65 | return 0, 0, 0, fmt.Errorf("asserted server version can'b contain negative values: %s", version) 66 | } else { 67 | return major, minor, update, nil 68 | } 69 | } 70 | 71 | // Validate checks the validity of the operators contents 72 | func (o *Operator) Validate(vr *ValidationResults) { 73 | if err := o.validateAccountServerURL(); err != nil { 74 | vr.AddError(err.Error()) 75 | } 76 | 77 | for _, v := range o.validateOperatorServiceURLs() { 78 | if v != nil { 79 | vr.AddError(v.Error()) 80 | } 81 | } 82 | 83 | for _, k := range o.SigningKeys { 84 | if !nkeys.IsValidPublicOperatorKey(k) { 85 | vr.AddError("%s is not an operator public key", k) 86 | } 87 | } 88 | if o.SystemAccount != "" { 89 | if !nkeys.IsValidPublicAccountKey(o.SystemAccount) { 90 | vr.AddError("%s is not an account public key", o.SystemAccount) 91 | } 92 | } 93 | if _, _, _, err := ParseServerVersion(o.AssertServerVersion); err != nil { 94 | vr.AddError("assert server version error: %s", err) 95 | } 96 | } 97 | 98 | func (o *Operator) validateAccountServerURL() error { 99 | if o.AccountServerURL != "" { 100 | // We don't care what kind of URL it is so long as it parses 101 | // and has a protocol. The account server may impose additional 102 | // constraints on the type of URLs that it is able to notify to 103 | u, err := url.Parse(o.AccountServerURL) 104 | if err != nil { 105 | return fmt.Errorf("error parsing account server url: %v", err) 106 | } 107 | if u.Scheme == "" { 108 | return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL) 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | // ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url. 115 | func ValidateOperatorServiceURL(v string) error { 116 | // should be possible for the service url to not be expressed 117 | if v == "" { 118 | return nil 119 | } 120 | u, err := url.Parse(v) 121 | if err != nil { 122 | return fmt.Errorf("error parsing operator service url %q: %v", v, err) 123 | } 124 | 125 | if u.User != nil { 126 | return fmt.Errorf("operator service url %q - credentials are not supported", v) 127 | } 128 | 129 | if u.Path != "" { 130 | return fmt.Errorf("operator service url %q - paths are not supported", v) 131 | } 132 | 133 | lcs := strings.ToLower(u.Scheme) 134 | switch lcs { 135 | case "nats": 136 | return nil 137 | case "tls": 138 | return nil 139 | case "ws": 140 | return nil 141 | case "wss": 142 | return nil 143 | default: 144 | return fmt.Errorf("operator service url %q - protocol not supported (only 'nats', 'tls', 'ws', 'wss' only)", v) 145 | } 146 | } 147 | 148 | func (o *Operator) validateOperatorServiceURLs() []error { 149 | var errs []error 150 | for _, v := range o.OperatorServiceURLs { 151 | if v != "" { 152 | if err := ValidateOperatorServiceURL(v); err != nil { 153 | errs = append(errs, err) 154 | } 155 | } 156 | } 157 | return errs 158 | } 159 | 160 | // OperatorClaims define the data for an operator JWT 161 | type OperatorClaims struct { 162 | ClaimsData 163 | Operator `json:"nats,omitempty"` 164 | } 165 | 166 | // NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key 167 | func NewOperatorClaims(subject string) *OperatorClaims { 168 | if subject == "" { 169 | return nil 170 | } 171 | c := &OperatorClaims{} 172 | c.Subject = subject 173 | c.Issuer = subject 174 | return c 175 | } 176 | 177 | // DidSign checks the claims against the operator's public key and its signing keys 178 | func (oc *OperatorClaims) DidSign(op Claims) bool { 179 | if op == nil { 180 | return false 181 | } 182 | issuer := op.Claims().Issuer 183 | if issuer == oc.Subject { 184 | if !oc.StrictSigningKeyUsage { 185 | return true 186 | } 187 | return op.Claims().Subject == oc.Subject 188 | } 189 | return oc.SigningKeys.Contains(issuer) 190 | } 191 | 192 | // Encode the claims into a JWT string 193 | func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { 194 | return oc.EncodeWithSigner(pair, nil) 195 | } 196 | 197 | func (oc *OperatorClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { 198 | if !nkeys.IsValidPublicOperatorKey(oc.Subject) { 199 | return "", errors.New("expected subject to be an operator public key") 200 | } 201 | err := oc.validateAccountServerURL() 202 | if err != nil { 203 | return "", err 204 | } 205 | oc.Type = OperatorClaim 206 | return oc.ClaimsData.encode(pair, oc, fn) 207 | } 208 | 209 | func (oc *OperatorClaims) ClaimType() ClaimType { 210 | return oc.Type 211 | } 212 | 213 | // DecodeOperatorClaims tries to create an operator claims from a JWt string 214 | func DecodeOperatorClaims(token string) (*OperatorClaims, error) { 215 | claims, err := Decode(token) 216 | if err != nil { 217 | return nil, err 218 | } 219 | oc, ok := claims.(*OperatorClaims) 220 | if !ok { 221 | return nil, errors.New("not operator claim") 222 | } 223 | return oc, nil 224 | } 225 | 226 | func (oc *OperatorClaims) String() string { 227 | return oc.ClaimsData.String(oc) 228 | } 229 | 230 | // Payload returns the operator specific data for an operator JWT 231 | func (oc *OperatorClaims) Payload() interface{} { 232 | return &oc.Operator 233 | } 234 | 235 | // Validate the contents of the claims 236 | func (oc *OperatorClaims) Validate(vr *ValidationResults) { 237 | oc.ClaimsData.Validate(vr) 238 | oc.Operator.Validate(vr) 239 | } 240 | 241 | // ExpectedPrefixes defines the nkey types that can sign operator claims, operator 242 | func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte { 243 | return []nkeys.PrefixByte{nkeys.PrefixByteOperator} 244 | } 245 | 246 | // Claims returns the generic claims data 247 | func (oc *OperatorClaims) Claims() *ClaimsData { 248 | return &oc.ClaimsData 249 | } 250 | 251 | func (oc *OperatorClaims) updateVersion() { 252 | oc.GenericFields.Version = libVersion 253 | } 254 | 255 | func (oc *OperatorClaims) GetTags() TagList { 256 | return oc.Operator.Tags 257 | } 258 | -------------------------------------------------------------------------------- /v2/revocation_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "time" 20 | ) 21 | 22 | const All = "*" 23 | 24 | // RevocationList is used to store a mapping of public keys to unix timestamps 25 | type RevocationList map[string]int64 26 | type RevocationEntry struct { 27 | PublicKey string 28 | TimeStamp int64 29 | } 30 | 31 | // Revoke enters a revocation by publickey and timestamp into this export 32 | // If there is already a revocation for this public key that is newer, it is kept. 33 | func (r RevocationList) Revoke(pubKey string, timestamp time.Time) { 34 | newTS := timestamp.Unix() 35 | // cannot move a revocation into the future - only into the past 36 | if ts, ok := r[pubKey]; ok && ts > newTS { 37 | return 38 | } 39 | r[pubKey] = newTS 40 | } 41 | 42 | // MaybeCompact will compact the revocation list if jwt.All is found. Any 43 | // revocation that is covered by a jwt.All revocation will be deleted, thus 44 | // reducing the size of the JWT. Returns a slice of entries that were removed 45 | // during the process. 46 | func (r RevocationList) MaybeCompact() []RevocationEntry { 47 | var deleted []RevocationEntry 48 | ats, ok := r[All] 49 | if ok { 50 | for k, ts := range r { 51 | if k != All && ats >= ts { 52 | deleted = append(deleted, RevocationEntry{ 53 | PublicKey: k, 54 | TimeStamp: ts, 55 | }) 56 | delete(r, k) 57 | } 58 | } 59 | } 60 | return deleted 61 | } 62 | 63 | // ClearRevocation removes any revocation for the public key 64 | func (r RevocationList) ClearRevocation(pubKey string) { 65 | delete(r, pubKey) 66 | } 67 | 68 | // IsRevoked checks if the public key is in the revoked list with a timestamp later than 69 | // the one passed in. Generally this method is called with an issue time but other time's can 70 | // be used for testing. 71 | func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool { 72 | if r.allRevoked(timestamp) { 73 | return true 74 | } 75 | ts, ok := r[pubKey] 76 | return ok && ts >= timestamp.Unix() 77 | } 78 | 79 | // allRevoked returns true if All is set and the timestamp is later or same as the 80 | // one passed. This is called by IsRevoked. 81 | func (r RevocationList) allRevoked(timestamp time.Time) bool { 82 | ts, ok := r[All] 83 | return ok && ts >= timestamp.Unix() 84 | } 85 | -------------------------------------------------------------------------------- /v2/revocation_list_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "sort" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestRevocationCompact(t *testing.T) { 25 | a := NewAccountClaims(publicKey(createAccountNKey(t), t)) 26 | 27 | now := time.Now() 28 | var keys []string 29 | keys = append(keys, publicKey(createUserNKey(t), t)) 30 | keys = append(keys, publicKey(createUserNKey(t), t)) 31 | keys = append(keys, publicKey(createUserNKey(t), t)) 32 | sort.Strings(keys) 33 | a.Revocations = make(RevocationList) 34 | a.Revocations.Revoke(keys[0], now.Add(-time.Hour)) 35 | a.Revocations.Revoke(keys[1], now.Add(-time.Minute)) 36 | a.Revocations.Revoke(keys[2], now.Add(-time.Second)) 37 | // no change expected - there's no 38 | deleted := a.Revocations.MaybeCompact() 39 | if len(a.Revocations) != 3 || deleted != nil { 40 | t.Error("expected 3 revocations") 41 | } 42 | // should delete the first key 43 | a.Revocations.Revoke(All, now.Add(-time.Minute*30)) 44 | deleted = a.Revocations.MaybeCompact() 45 | if len(a.Revocations) != 3 && len(deleted) != 1 && deleted[0].PublicKey != keys[0] { 46 | t.Error("expected 3 revocations") 47 | } 48 | // should delete the 2 remaining keys, only All remains 49 | a.Revocations.Revoke(All, now.Add(-time.Second)) 50 | deleted = a.Revocations.MaybeCompact() 51 | if len(a.Revocations) != 1 && len(deleted) != 2 && deleted[0].PublicKey != keys[1] && deleted[1].PublicKey != keys[2] { 52 | t.Error("didn't revoke expected entries") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /v2/signingkeys.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "sort" 23 | 24 | "github.com/nats-io/nkeys" 25 | ) 26 | 27 | type Scope interface { 28 | SigningKey() string 29 | ValidateScopedSigner(claim Claims) error 30 | Validate(vr *ValidationResults) 31 | } 32 | 33 | type ScopeType int 34 | 35 | const ( 36 | UserScopeType ScopeType = iota + 1 37 | ) 38 | 39 | func (t ScopeType) String() string { 40 | switch t { 41 | case UserScopeType: 42 | return "user_scope" 43 | } 44 | return "unknown" 45 | } 46 | 47 | func (t *ScopeType) MarshalJSON() ([]byte, error) { 48 | switch *t { 49 | case UserScopeType: 50 | return []byte("\"user_scope\""), nil 51 | } 52 | return nil, fmt.Errorf("unknown scope type %q", t) 53 | } 54 | 55 | func (t *ScopeType) UnmarshalJSON(b []byte) error { 56 | var s string 57 | err := json.Unmarshal(b, &s) 58 | if err != nil { 59 | return err 60 | } 61 | switch s { 62 | case "user_scope": 63 | *t = UserScopeType 64 | return nil 65 | } 66 | return fmt.Errorf("unknown scope type %q", t) 67 | } 68 | 69 | type UserScope struct { 70 | Kind ScopeType `json:"kind"` 71 | Key string `json:"key"` 72 | Role string `json:"role"` 73 | Template UserPermissionLimits `json:"template"` 74 | Description string `json:"description"` 75 | } 76 | 77 | func NewUserScope() *UserScope { 78 | var s UserScope 79 | s.Kind = UserScopeType 80 | s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit} 81 | return &s 82 | } 83 | 84 | func (us UserScope) SigningKey() string { 85 | return us.Key 86 | } 87 | 88 | func (us UserScope) Validate(vr *ValidationResults) { 89 | if !nkeys.IsValidPublicAccountKey(us.Key) { 90 | vr.AddError("%s is not an account public key", us.Key) 91 | } 92 | } 93 | 94 | func (us UserScope) ValidateScopedSigner(c Claims) error { 95 | uc, ok := c.(*UserClaims) 96 | if !ok { 97 | return fmt.Errorf("not an user claim - scoped signing key requires user claim") 98 | } 99 | if uc.Claims().Issuer != us.Key { 100 | return errors.New("issuer not the scoped signer") 101 | } 102 | if !uc.HasEmptyPermissions() { 103 | return errors.New("scoped users require no permissions or limits set") 104 | } 105 | return nil 106 | } 107 | 108 | // SigningKeys is a map keyed by a public account key 109 | type SigningKeys map[string]Scope 110 | 111 | func (sk SigningKeys) Validate(vr *ValidationResults) { 112 | for k, v := range sk { 113 | // regular signing keys won't have a scope 114 | if v != nil { 115 | v.Validate(vr) 116 | } else { 117 | if !nkeys.IsValidPublicAccountKey(k) { 118 | vr.AddError("%q is not a valid account signing key", k) 119 | } 120 | } 121 | } 122 | } 123 | 124 | // MarshalJSON serializes the scoped signing keys as an array 125 | func (sk *SigningKeys) MarshalJSON() ([]byte, error) { 126 | if sk == nil { 127 | return nil, nil 128 | } 129 | 130 | keys := sk.Keys() 131 | sort.Strings(keys) 132 | 133 | var a []interface{} 134 | for _, k := range keys { 135 | if (*sk)[k] != nil { 136 | a = append(a, (*sk)[k]) 137 | } else { 138 | a = append(a, k) 139 | } 140 | } 141 | return json.Marshal(a) 142 | } 143 | 144 | func (sk *SigningKeys) UnmarshalJSON(data []byte) error { 145 | if *sk == nil { 146 | *sk = make(SigningKeys) 147 | } 148 | // read an array - we can have a string or an map 149 | var a []interface{} 150 | if err := json.Unmarshal(data, &a); err != nil { 151 | return err 152 | } 153 | for _, i := range a { 154 | switch v := i.(type) { 155 | case string: 156 | (*sk)[v] = nil 157 | case map[string]interface{}: 158 | d, err := json.Marshal(v) 159 | if err != nil { 160 | return err 161 | } 162 | switch v["kind"] { 163 | case UserScopeType.String(): 164 | us := NewUserScope() 165 | if err := json.Unmarshal(d, &us); err != nil { 166 | return err 167 | } 168 | (*sk)[us.Key] = us 169 | default: 170 | return fmt.Errorf("unknown signing key scope %q", v["type"]) 171 | } 172 | } 173 | } 174 | return nil 175 | } 176 | 177 | func (sk SigningKeys) Keys() []string { 178 | var keys []string 179 | for k := range sk { 180 | keys = append(keys, k) 181 | } 182 | return keys 183 | } 184 | 185 | // GetScope returns nil if the key is not associated 186 | func (sk SigningKeys) GetScope(k string) (Scope, bool) { 187 | v, ok := sk[k] 188 | if !ok { 189 | return nil, false 190 | } 191 | return v, true 192 | } 193 | 194 | func (sk SigningKeys) Contains(k string) bool { 195 | _, ok := sk[k] 196 | return ok 197 | } 198 | 199 | func (sk SigningKeys) Add(keys ...string) { 200 | for _, k := range keys { 201 | sk[k] = nil 202 | } 203 | } 204 | 205 | func (sk SigningKeys) AddScopedSigner(s Scope) { 206 | sk[s.SigningKey()] = s 207 | } 208 | 209 | func (sk SigningKeys) Remove(keys ...string) { 210 | for _, k := range keys { 211 | delete(sk, k) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /v2/test/genericclaims_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | . "github.com/nats-io/jwt/v2" 23 | jwtv1 "github.com/nats-io/jwt/v2/v1compat" 24 | ) 25 | 26 | func TestNewGenericClaims(t *testing.T) { 27 | akp := createAccountNKey(t) 28 | apk := publicKey(akp, t) 29 | 30 | gc := NewGenericClaims(apk) 31 | gc.Expires = time.Now().Add(time.Hour).UTC().Unix() 32 | gc.Name = "alberto" 33 | gc.Audience = "everyone" 34 | gc.NotBefore = time.Now().UTC().Unix() 35 | gc.Data["test"] = true 36 | 37 | gcJwt := encode(gc, akp, t) 38 | 39 | uc2, err := DecodeGeneric(gcJwt) 40 | if err != nil { 41 | t.Fatal("failed to decode", err) 42 | } 43 | 44 | AssertEquals(gc.String(), uc2.String(), t) 45 | AssertEquals(gc.Name, uc2.Name, t) 46 | AssertEquals(gc.Audience, uc2.Audience, t) 47 | AssertEquals(gc.Expires, uc2.Expires, t) 48 | AssertEquals(gc.NotBefore, uc2.NotBefore, t) 49 | AssertEquals(gc.Subject, uc2.Subject, t) 50 | 51 | AssertEquals(gc.Data["test"], true, t) 52 | AssertEquals(gc.Claims() != nil, true, t) 53 | AssertEquals(gc.Payload() != nil, true, t) 54 | } 55 | 56 | func TestNewGenericOperatorClaims(t *testing.T) { 57 | okp := createOperatorNKey(t) 58 | opk := publicKey(okp, t) 59 | 60 | op := NewOperatorClaims(opk) 61 | 62 | oJwt := encode(op, okp, t) 63 | 64 | oc2, err := DecodeGeneric(oJwt) 65 | if err != nil { 66 | t.Fatal("failed to decode", err) 67 | } 68 | if OperatorClaim != oc2.ClaimType() { 69 | t.Fatalf("Bad Claim type") 70 | } 71 | } 72 | 73 | func TestGenericClaimsCanHaveCustomType(t *testing.T) { 74 | akp := createAccountNKey(t) 75 | apk := publicKey(akp, t) 76 | 77 | gc := NewGenericClaims(apk) 78 | gc.Expires = time.Now().Add(time.Hour).UTC().Unix() 79 | gc.Name = "alberto" 80 | gc.Data["hello"] = "world" 81 | gc.Data["count"] = 5 82 | gc.Data["type"] = "my_type" 83 | gcJwt := encode(gc, akp, t) 84 | 85 | gc2, err := DecodeGeneric(gcJwt) 86 | if err != nil { 87 | t.Fatal("failed to decode", err) 88 | } 89 | if gc2.ClaimType() != GenericClaim { 90 | t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) 91 | } 92 | if gc2.Data["type"] != "my_type" { 93 | t.Fatalf("expected internal type to be 'my_type': %v", gc2.Data["type"]) 94 | } 95 | } 96 | 97 | func TestGenericClaimsCanHaveCustomTypeFromV1(t *testing.T) { 98 | akp := createAccountNKey(t) 99 | apk := publicKey(akp, t) 100 | 101 | gc := jwtv1.NewGenericClaims(apk) 102 | gc.Expires = time.Now().Add(time.Hour).UTC().Unix() 103 | gc.Name = "alberto" 104 | gc.Data["hello"] = "world" 105 | gc.Data["count"] = 5 106 | gc.Type = "my_type" 107 | token, err := gc.Encode(akp) 108 | if err != nil { 109 | t.Fatalf("failed to encode v1 JWT: %v", err) 110 | } 111 | 112 | gc2, err := DecodeGeneric(token) 113 | if err != nil { 114 | t.Fatal("failed to decode", err) 115 | } 116 | if gc2.ClaimType() != GenericClaim { 117 | t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) 118 | } 119 | if gc2.Data["type"] != "my_type" { 120 | t.Fatalf("expected internal type to be 'my_type': %v", gc2.Data["type"]) 121 | } 122 | } 123 | 124 | func TestGenericClaimsSignerFn(t *testing.T) { 125 | akp := createAccountNKey(t) 126 | apk := publicKey(akp, t) 127 | 128 | gc := NewGenericClaims(apk) 129 | gc.Expires = time.Now().Add(time.Hour).UTC().Unix() 130 | gc.Name = "alberto" 131 | gc.Data["hello"] = "world" 132 | gc.Data["count"] = 5 133 | gc.Data["type"] = "my_type" 134 | 135 | ok := false 136 | gcJwt, err := gc.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { 137 | ok = true 138 | return akp.Sign(data) 139 | }) 140 | if err != nil { 141 | t.Fatal("failed to encode") 142 | } 143 | if !ok { 144 | t.Fatal("didn't encode with function") 145 | } 146 | 147 | gc2, err := DecodeGeneric(gcJwt) 148 | if err != nil { 149 | t.Fatal("failed to decode", err) 150 | } 151 | if gc2.ClaimType() != GenericClaim { 152 | t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /v2/test/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2021 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/nats-io/nkeys" 26 | 27 | . "github.com/nats-io/jwt/v2" 28 | ) 29 | 30 | func Trace(message string) string { 31 | lines := make([]string, 0, 32) 32 | err := errors.New(message) 33 | msg := err.Error() 34 | lines = append(lines, msg) 35 | 36 | for i := 2; true; i++ { 37 | _, file, line, ok := runtime.Caller(i) 38 | if !ok { 39 | break 40 | } 41 | msg := fmt.Sprintf("%s:%d", file, line) 42 | lines = append(lines, msg) 43 | } 44 | return strings.Join(lines, "\n") 45 | } 46 | 47 | func AssertEquals(expected, v interface{}, t *testing.T) { 48 | if expected != v { 49 | t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) 50 | } 51 | } 52 | 53 | func AssertNil(v interface{}, t *testing.T) { 54 | if v != nil { 55 | t.FailNow() 56 | } 57 | } 58 | 59 | func AssertNoError(err error, t *testing.T) { 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | } 64 | 65 | func AssertTrue(condition bool, t *testing.T) { 66 | if !condition { 67 | t.FailNow() 68 | } 69 | } 70 | 71 | func AssertFalse(condition bool, t *testing.T) { 72 | if condition { 73 | t.FailNow() 74 | } 75 | } 76 | 77 | func createAccountNKey(t *testing.T) nkeys.KeyPair { 78 | kp, err := nkeys.CreateAccount() 79 | if err != nil { 80 | t.Fatal("error creating account kp", err) 81 | } 82 | return kp 83 | } 84 | 85 | func createOperatorNKey(t *testing.T) nkeys.KeyPair { 86 | kp, err := nkeys.CreateOperator() 87 | if err != nil { 88 | t.Fatal("error creating operator kp", err) 89 | } 90 | return kp 91 | } 92 | 93 | func publicKey(kp nkeys.KeyPair, t *testing.T) string { 94 | pk, err := kp.PublicKey() 95 | if err != nil { 96 | t.Fatal("error reading public key", err) 97 | } 98 | return pk 99 | } 100 | 101 | func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { 102 | s, err := c.Encode(kp) 103 | if err != nil { 104 | t.Fatal("error encoding claim", err) 105 | } 106 | return s 107 | } 108 | -------------------------------------------------------------------------------- /v2/user_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2024 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "reflect" 21 | 22 | "github.com/nats-io/nkeys" 23 | ) 24 | 25 | const ( 26 | ConnectionTypeStandard = "STANDARD" 27 | ConnectionTypeWebsocket = "WEBSOCKET" 28 | ConnectionTypeLeafnode = "LEAFNODE" 29 | ConnectionTypeLeafnodeWS = "LEAFNODE_WS" 30 | ConnectionTypeMqtt = "MQTT" 31 | ConnectionTypeMqttWS = "MQTT_WS" 32 | ConnectionTypeInProcess = "IN_PROCESS" 33 | ) 34 | 35 | type UserPermissionLimits struct { 36 | Permissions 37 | Limits 38 | BearerToken bool `json:"bearer_token,omitempty"` 39 | AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"` 40 | } 41 | 42 | // User defines the user specific data in a user JWT 43 | type User struct { 44 | UserPermissionLimits 45 | // IssuerAccount stores the public key for the account the issuer represents. 46 | // When set, the claim was issued by a signing key. 47 | IssuerAccount string `json:"issuer_account,omitempty"` 48 | GenericFields 49 | } 50 | 51 | // Validate checks the permissions and limits in a User jwt 52 | func (u *User) Validate(vr *ValidationResults) { 53 | u.Permissions.Validate(vr) 54 | u.Limits.Validate(vr) 55 | // When BearerToken is true server will ignore any nonce-signing verification 56 | } 57 | 58 | // UserClaims defines a user JWT 59 | type UserClaims struct { 60 | ClaimsData 61 | User `json:"nats,omitempty"` 62 | } 63 | 64 | // NewUserClaims creates a user JWT with the specific subject/public key 65 | func NewUserClaims(subject string) *UserClaims { 66 | if subject == "" { 67 | return nil 68 | } 69 | c := &UserClaims{} 70 | c.Subject = subject 71 | c.Limits = Limits{ 72 | UserLimits{CIDRList{}, nil, ""}, 73 | NatsLimits{NoLimit, NoLimit, NoLimit}, 74 | } 75 | return c 76 | } 77 | 78 | func (u *UserClaims) SetScoped(t bool) { 79 | if t { 80 | u.UserPermissionLimits = UserPermissionLimits{} 81 | } else { 82 | u.Limits = Limits{ 83 | UserLimits{CIDRList{}, nil, ""}, 84 | NatsLimits{NoLimit, NoLimit, NoLimit}, 85 | } 86 | } 87 | } 88 | 89 | func (u *UserClaims) HasEmptyPermissions() bool { 90 | return reflect.DeepEqual(u.UserPermissionLimits, UserPermissionLimits{}) 91 | } 92 | 93 | // Encode tries to turn the user claims into a JWT string 94 | func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) { 95 | return u.EncodeWithSigner(pair, nil) 96 | } 97 | 98 | func (u *UserClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { 99 | if !nkeys.IsValidPublicUserKey(u.Subject) { 100 | return "", errors.New("expected subject to be user public key") 101 | } 102 | u.Type = UserClaim 103 | return u.ClaimsData.encode(pair, u, fn) 104 | } 105 | 106 | // DecodeUserClaims tries to parse a user claims from a JWT string 107 | func DecodeUserClaims(token string) (*UserClaims, error) { 108 | claims, err := Decode(token) 109 | if err != nil { 110 | return nil, err 111 | } 112 | ac, ok := claims.(*UserClaims) 113 | if !ok { 114 | return nil, errors.New("not user claim") 115 | } 116 | return ac, nil 117 | } 118 | 119 | func (u *UserClaims) ClaimType() ClaimType { 120 | return u.Type 121 | } 122 | 123 | // Validate checks the generic and specific parts of the user jwt 124 | func (u *UserClaims) Validate(vr *ValidationResults) { 125 | u.ClaimsData.Validate(vr) 126 | u.User.Validate(vr) 127 | if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) { 128 | vr.AddError("account_id is not an account public key") 129 | } 130 | } 131 | 132 | // ExpectedPrefixes defines the types that can encode a user JWT, account 133 | func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte { 134 | return []nkeys.PrefixByte{nkeys.PrefixByteAccount} 135 | } 136 | 137 | // Claims returns the generic data from a user jwt 138 | func (u *UserClaims) Claims() *ClaimsData { 139 | return &u.ClaimsData 140 | } 141 | 142 | // Payload returns the user specific data from a user JWT 143 | func (u *UserClaims) Payload() interface{} { 144 | return &u.User 145 | } 146 | 147 | func (u *UserClaims) String() string { 148 | return u.ClaimsData.String(u) 149 | } 150 | 151 | func (u *UserClaims) updateVersion() { 152 | u.GenericFields.Version = libVersion 153 | } 154 | 155 | // IsBearerToken returns true if nonce-signing requirements should be skipped 156 | func (u *UserClaims) IsBearerToken() bool { 157 | return u.BearerToken 158 | } 159 | 160 | func (u *UserClaims) GetTags() TagList { 161 | return u.User.Tags 162 | } 163 | -------------------------------------------------------------------------------- /v2/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | func Trace(message string) string { 29 | lines := make([]string, 0, 32) 30 | err := errors.New(message) 31 | msg := err.Error() 32 | lines = append(lines, msg) 33 | 34 | for i := 2; true; i++ { 35 | _, file, line, ok := runtime.Caller(i) 36 | if !ok { 37 | break 38 | } 39 | msg := fmt.Sprintf("%s:%d", file, line) 40 | lines = append(lines, msg) 41 | } 42 | return strings.Join(lines, "\n") 43 | } 44 | 45 | func AssertEquals(expected, v interface{}, t *testing.T) { 46 | if expected != v { 47 | t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) 48 | } 49 | } 50 | 51 | func AssertNil(v interface{}, t *testing.T) { 52 | if v != nil { 53 | t.FailNow() 54 | } 55 | } 56 | 57 | func AssertNoError(err error, t *testing.T) { 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | } 62 | 63 | func AssertTrue(condition bool, t *testing.T) { 64 | if !condition { 65 | t.FailNow() 66 | } 67 | } 68 | 69 | func AssertFalse(condition bool, t *testing.T) { 70 | if condition { 71 | t.FailNow() 72 | } 73 | } 74 | 75 | func createAccountNKey(t *testing.T) nkeys.KeyPair { 76 | kp, err := nkeys.CreateAccount() 77 | if err != nil { 78 | t.Fatal("error creating account kp", err) 79 | } 80 | return kp 81 | } 82 | 83 | func createUserNKey(t *testing.T) nkeys.KeyPair { 84 | kp, err := nkeys.CreateUser() 85 | if err != nil { 86 | t.Fatal("error creating account kp", err) 87 | } 88 | return kp 89 | } 90 | 91 | func createOperatorNKey(t *testing.T) nkeys.KeyPair { 92 | kp, err := nkeys.CreateOperator() 93 | if err != nil { 94 | t.Fatal("error creating operator kp", err) 95 | } 96 | return kp 97 | } 98 | 99 | func createServerNKey(t *testing.T) nkeys.KeyPair { 100 | kp, err := nkeys.CreateServer() 101 | if err != nil { 102 | t.Fatal("error creating server kp", err) 103 | } 104 | return kp 105 | } 106 | 107 | func createClusterNKey(t *testing.T) nkeys.KeyPair { 108 | kp, err := nkeys.CreateCluster() 109 | if err != nil { 110 | t.Fatal("error creating cluster kp", err) 111 | } 112 | return kp 113 | } 114 | 115 | func publicKey(kp nkeys.KeyPair, t *testing.T) string { 116 | pk, err := kp.PublicKey() 117 | if err != nil { 118 | t.Fatal("error reading public key", err) 119 | } 120 | return pk 121 | } 122 | 123 | func seedKey(kp nkeys.KeyPair, t *testing.T) []byte { 124 | sk, err := kp.Seed() 125 | if err != nil { 126 | t.Fatal("error reading seed", err) 127 | } 128 | return sk 129 | } 130 | 131 | func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { 132 | s, err := c.Encode(kp) 133 | if err != nil { 134 | t.Fatal("error encoding claim", err) 135 | } 136 | return s 137 | } 138 | -------------------------------------------------------------------------------- /v2/v1compat/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test cover 2 | 3 | build: 4 | go build 5 | 6 | test: 7 | ../../../scripts/test.sh 8 | 9 | fmt: 10 | gofmt -w -s *.go 11 | go mod tidy 12 | cd v2/ 13 | gofmt -w -s *.go 14 | go mod tidy 15 | 16 | cover: 17 | go test -v -covermode=count -coverprofile=coverage.out 18 | go tool cover -html=coverage.out 19 | -------------------------------------------------------------------------------- /v2/v1compat/account_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2023 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "sort" 21 | "time" 22 | 23 | "github.com/nats-io/nkeys" 24 | ) 25 | 26 | // NoLimit is used to indicate a limit field is unlimited in value. 27 | const NoLimit = -1 28 | 29 | // OperatorLimits are used to limit access by an account 30 | type OperatorLimits struct { 31 | Subs int64 `json:"subs,omitempty"` // Max number of subscriptions 32 | Conn int64 `json:"conn,omitempty"` // Max number of active connections 33 | LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections 34 | Imports int64 `json:"imports,omitempty"` // Max number of imports 35 | Exports int64 `json:"exports,omitempty"` // Max number of exports 36 | Data int64 `json:"data,omitempty"` // Max number of bytes 37 | Payload int64 `json:"payload,omitempty"` // Max message payload 38 | WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports 39 | } 40 | 41 | // IsEmpty returns true if all of the limits are 0/false. 42 | func (o *OperatorLimits) IsEmpty() bool { 43 | return *o == OperatorLimits{} 44 | } 45 | 46 | // IsUnlimited returns true if all limits are 47 | func (o *OperatorLimits) IsUnlimited() bool { 48 | return *o == OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true} 49 | } 50 | 51 | // Validate checks that the operator limits contain valid values 52 | func (o *OperatorLimits) Validate(vr *ValidationResults) { 53 | // negative values mean unlimited, so all numbers are valid 54 | } 55 | 56 | // Account holds account specific claims data 57 | type Account struct { 58 | Imports Imports `json:"imports,omitempty"` 59 | Exports Exports `json:"exports,omitempty"` 60 | Identities []Identity `json:"identity,omitempty"` 61 | Limits OperatorLimits `json:"limits,omitempty"` 62 | SigningKeys StringList `json:"signing_keys,omitempty"` 63 | Revocations RevocationList `json:"revocations,omitempty"` 64 | } 65 | 66 | // Validate checks if the account is valid, based on the wrapper 67 | func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) { 68 | a.Imports.Validate(acct.Subject, vr) 69 | a.Exports.Validate(vr) 70 | a.Limits.Validate(vr) 71 | 72 | for _, i := range a.Identities { 73 | i.Validate(vr) 74 | } 75 | 76 | if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports { 77 | vr.AddError("the account contains more imports than allowed by the operator") 78 | } 79 | 80 | // Check Imports and Exports for limit violations. 81 | if a.Limits.Imports != NoLimit { 82 | if int64(len(a.Imports)) > a.Limits.Imports { 83 | vr.AddError("the account contains more imports than allowed by the operator") 84 | } 85 | } 86 | if a.Limits.Exports != NoLimit { 87 | if int64(len(a.Exports)) > a.Limits.Exports { 88 | vr.AddError("the account contains more exports than allowed by the operator") 89 | } 90 | // Check for wildcard restrictions 91 | if !a.Limits.WildcardExports { 92 | for _, ex := range a.Exports { 93 | if ex.Subject.HasWildCards() { 94 | vr.AddError("the account contains wildcard exports that are not allowed by the operator") 95 | } 96 | } 97 | } 98 | } 99 | 100 | for _, k := range a.SigningKeys { 101 | if !nkeys.IsValidPublicAccountKey(k) { 102 | vr.AddError("%s is not an account public key", k) 103 | } 104 | } 105 | } 106 | 107 | // AccountClaims defines the body of an account JWT 108 | type AccountClaims struct { 109 | ClaimsData 110 | Account `json:"nats,omitempty"` 111 | } 112 | 113 | // NewAccountClaims creates a new account JWT 114 | func NewAccountClaims(subject string) *AccountClaims { 115 | if subject == "" { 116 | return nil 117 | } 118 | c := &AccountClaims{} 119 | // Set to unlimited to start. We do it this way so we get compiler 120 | // errors if we add to the OperatorLimits. 121 | c.Limits = OperatorLimits{NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, NoLimit, true} 122 | c.Subject = subject 123 | return c 124 | } 125 | 126 | // Encode converts account claims into a JWT string 127 | func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) { 128 | if !nkeys.IsValidPublicAccountKey(a.Subject) { 129 | return "", errors.New("expected subject to be account public key") 130 | } 131 | sort.Sort(a.Exports) 132 | sort.Sort(a.Imports) 133 | a.ClaimsData.Type = AccountClaim 134 | return a.ClaimsData.Encode(pair, a) 135 | } 136 | 137 | // DecodeAccountClaims decodes account claims from a JWT string 138 | func DecodeAccountClaims(token string) (*AccountClaims, error) { 139 | v := AccountClaims{} 140 | if err := Decode(token, &v); err != nil { 141 | return nil, err 142 | } 143 | return &v, nil 144 | } 145 | 146 | func (a *AccountClaims) String() string { 147 | return a.ClaimsData.String(a) 148 | } 149 | 150 | // Payload pulls the accounts specific payload out of the claims 151 | func (a *AccountClaims) Payload() interface{} { 152 | return &a.Account 153 | } 154 | 155 | // Validate checks the accounts contents 156 | func (a *AccountClaims) Validate(vr *ValidationResults) { 157 | a.ClaimsData.Validate(vr) 158 | a.Account.Validate(a, vr) 159 | 160 | if nkeys.IsValidPublicAccountKey(a.ClaimsData.Issuer) { 161 | if len(a.Identities) > 0 { 162 | vr.AddWarning("self-signed account JWTs shouldn't contain identity proofs") 163 | } 164 | if !a.Limits.IsEmpty() { 165 | vr.AddWarning("self-signed account JWTs shouldn't contain operator limits") 166 | } 167 | } 168 | } 169 | 170 | // ExpectedPrefixes defines the types that can encode an account jwt, account and operator 171 | func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte { 172 | return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} 173 | } 174 | 175 | // Claims returns the accounts claims data 176 | func (a *AccountClaims) Claims() *ClaimsData { 177 | return &a.ClaimsData 178 | } 179 | 180 | // DidSign checks the claims against the account's public key and its signing keys 181 | func (a *AccountClaims) DidSign(c Claims) bool { 182 | if c != nil { 183 | issuer := c.Claims().Issuer 184 | if issuer == a.Subject { 185 | return true 186 | } 187 | uc, ok := c.(*UserClaims) 188 | if ok && uc.IssuerAccount == a.Subject { 189 | return a.SigningKeys.Contains(issuer) 190 | } 191 | at, ok := c.(*ActivationClaims) 192 | if ok && at.IssuerAccount == a.Subject { 193 | return a.SigningKeys.Contains(issuer) 194 | } 195 | } 196 | return false 197 | } 198 | 199 | // Revoke enters a revocation by publickey using time.Now(). 200 | func (a *AccountClaims) Revoke(pubKey string) { 201 | a.RevokeAt(pubKey, time.Now()) 202 | } 203 | 204 | // RevokeAt enters a revocation by public key and timestamp into this account 205 | // This will revoke all jwt issued for pubKey, prior to timestamp 206 | // If there is already a revocation for this public key that is newer, it is kept. 207 | func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) { 208 | if a.Revocations == nil { 209 | a.Revocations = RevocationList{} 210 | } 211 | 212 | a.Revocations.Revoke(pubKey, timestamp) 213 | } 214 | 215 | // ClearRevocation removes any revocation for the public key 216 | func (a *AccountClaims) ClearRevocation(pubKey string) { 217 | a.Revocations.ClearRevocation(pubKey) 218 | } 219 | 220 | // IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in. 221 | // Generally this method is called with the subject and issue time of the jwt to be tested. 222 | // DO NOT pass time.Now(), it will not produce a stable/expected response. 223 | // The value is expected to be a public key or "*" (means all public keys) 224 | func (a *AccountClaims) IsRevokedAt(pubKey string, timestamp time.Time) bool { 225 | return a.Revocations.IsRevoked(pubKey, timestamp) 226 | } 227 | 228 | // IsRevoked does not perform a valid check. Use IsRevokedAt instead. 229 | func (a *AccountClaims) IsRevoked(_ string) bool { 230 | return true 231 | } 232 | 233 | // IsClaimRevoked checks if the account revoked the claim passed in. 234 | // Invalid claims (nil, no Subject or IssuedAt) will return true. 235 | func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool { 236 | if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" { 237 | return true 238 | } 239 | return a.Revocations.IsRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0)) 240 | } 241 | -------------------------------------------------------------------------------- /v2/v1compat/activation_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "crypto/sha256" 20 | "encoding/base32" 21 | "errors" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | // Activation defines the custom parts of an activation claim 29 | type Activation struct { 30 | ImportSubject Subject `json:"subject,omitempty"` 31 | ImportType ExportType `json:"type,omitempty"` 32 | Limits 33 | } 34 | 35 | // IsService returns true if an Activation is for a service 36 | func (a *Activation) IsService() bool { 37 | return a.ImportType == Service 38 | } 39 | 40 | // IsStream returns true if an Activation is for a stream 41 | func (a *Activation) IsStream() bool { 42 | return a.ImportType == Stream 43 | } 44 | 45 | // Validate checks the exports and limits in an activation JWT 46 | func (a *Activation) Validate(vr *ValidationResults) { 47 | if !a.IsService() && !a.IsStream() { 48 | vr.AddError("invalid export type: %q", a.ImportType) 49 | } 50 | 51 | if a.IsService() { 52 | if a.ImportSubject.HasWildCards() { 53 | vr.AddError("services cannot have wildcard subject: %q", a.ImportSubject) 54 | } 55 | } 56 | 57 | a.ImportSubject.Validate(vr) 58 | a.Limits.Validate(vr) 59 | } 60 | 61 | // ActivationClaims holds the data specific to an activation JWT 62 | type ActivationClaims struct { 63 | ClaimsData 64 | Activation `json:"nats,omitempty"` 65 | // IssuerAccount stores the public key for the account the issuer represents. 66 | // When set, the claim was issued by a signing key. 67 | IssuerAccount string `json:"issuer_account,omitempty"` 68 | } 69 | 70 | // NewActivationClaims creates a new activation claim with the provided sub 71 | func NewActivationClaims(subject string) *ActivationClaims { 72 | if subject == "" { 73 | return nil 74 | } 75 | ac := &ActivationClaims{} 76 | ac.Subject = subject 77 | return ac 78 | } 79 | 80 | // Encode turns an activation claim into a JWT strimg 81 | func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) { 82 | if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) { 83 | return "", errors.New("expected subject to be an account") 84 | } 85 | a.ClaimsData.Type = ActivationClaim 86 | return a.ClaimsData.Encode(pair, a) 87 | } 88 | 89 | // DecodeActivationClaims tries to create an activation claim from a JWT string 90 | func DecodeActivationClaims(token string) (*ActivationClaims, error) { 91 | v := ActivationClaims{} 92 | if err := Decode(token, &v); err != nil { 93 | return nil, err 94 | } 95 | return &v, nil 96 | } 97 | 98 | // Payload returns the activation specific part of the JWT 99 | func (a *ActivationClaims) Payload() interface{} { 100 | return a.Activation 101 | } 102 | 103 | // Validate checks the claims 104 | func (a *ActivationClaims) Validate(vr *ValidationResults) { 105 | a.validateWithTimeChecks(vr, true) 106 | } 107 | 108 | // Validate checks the claims 109 | func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) { 110 | if timeChecks { 111 | a.ClaimsData.Validate(vr) 112 | } 113 | a.Activation.Validate(vr) 114 | if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) { 115 | vr.AddError("account_id is not an account public key") 116 | } 117 | } 118 | 119 | // ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator 120 | func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte { 121 | return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator} 122 | } 123 | 124 | // Claims returns the generic part of the JWT 125 | func (a *ActivationClaims) Claims() *ClaimsData { 126 | return &a.ClaimsData 127 | } 128 | 129 | func (a *ActivationClaims) String() string { 130 | return a.ClaimsData.String(a) 131 | } 132 | 133 | // HashID returns a hash of the claims that can be used to identify it. 134 | // The hash is calculated by creating a string with 135 | // issuerPubKey.subjectPubKey. and constructing the sha-256 hash and base32 encoding that. 136 | // is the exported subject, minus any wildcards, so foo.* becomes foo. 137 | // the one special case is that if the export start with "*" or is ">" the "_" 138 | func (a *ActivationClaims) HashID() (string, error) { 139 | 140 | if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" { 141 | return "", fmt.Errorf("not enough data in the activaion claims to create a hash") 142 | } 143 | 144 | subject := cleanSubject(string(a.ImportSubject)) 145 | base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject) 146 | h := sha256.New() 147 | h.Write([]byte(base)) 148 | sha := h.Sum(nil) 149 | hash := base32.StdEncoding.EncodeToString(sha) 150 | 151 | return hash, nil 152 | } 153 | 154 | func cleanSubject(subject string) string { 155 | split := strings.Split(subject, ".") 156 | cleaned := "" 157 | 158 | for i, tok := range split { 159 | if tok == "*" || tok == ">" { 160 | if i == 0 { 161 | cleaned = "_" 162 | break 163 | } 164 | 165 | cleaned = strings.Join(split[:i], ".") 166 | break 167 | } 168 | } 169 | if cleaned == "" { 170 | cleaned = subject 171 | } 172 | return cleaned 173 | } 174 | -------------------------------------------------------------------------------- /v2/v1compat/claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2019 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "crypto/sha512" 20 | "encoding/base32" 21 | "encoding/base64" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "strings" 26 | "time" 27 | 28 | "github.com/nats-io/nkeys" 29 | ) 30 | 31 | // ClaimType is used to indicate the type of JWT being stored in a Claim 32 | type ClaimType string 33 | 34 | const ( 35 | // AccountClaim is the type of an Account JWT 36 | AccountClaim = "account" 37 | //ActivationClaim is the type of an activation JWT 38 | ActivationClaim = "activation" 39 | //UserClaim is the type of an user JWT 40 | UserClaim = "user" 41 | //OperatorClaim is the type of an operator JWT 42 | OperatorClaim = "operator" 43 | 44 | //ServerClaim is the type of an server JWT 45 | // Deprecated: ServerClaim is not supported 46 | ServerClaim = "server" 47 | // ClusterClaim is the type of an cluster JWT 48 | // Deprecated: ClusterClaim is not supported 49 | ClusterClaim = "cluster" 50 | ) 51 | 52 | // Claims is a JWT claims 53 | type Claims interface { 54 | Claims() *ClaimsData 55 | Encode(kp nkeys.KeyPair) (string, error) 56 | ExpectedPrefixes() []nkeys.PrefixByte 57 | Payload() interface{} 58 | String() string 59 | Validate(vr *ValidationResults) 60 | Verify(payload string, sig []byte) bool 61 | } 62 | 63 | // ClaimsData is the base struct for all claims 64 | type ClaimsData struct { 65 | Audience string `json:"aud,omitempty"` 66 | Expires int64 `json:"exp,omitempty"` 67 | ID string `json:"jti,omitempty"` 68 | IssuedAt int64 `json:"iat,omitempty"` 69 | Issuer string `json:"iss,omitempty"` 70 | Name string `json:"name,omitempty"` 71 | NotBefore int64 `json:"nbf,omitempty"` 72 | Subject string `json:"sub,omitempty"` 73 | Tags TagList `json:"tags,omitempty"` 74 | Type ClaimType `json:"type,omitempty"` 75 | } 76 | 77 | // Prefix holds the prefix byte for an NKey 78 | type Prefix struct { 79 | nkeys.PrefixByte 80 | } 81 | 82 | func encodeToString(d []byte) string { 83 | return base64.RawURLEncoding.EncodeToString(d) 84 | } 85 | 86 | func decodeString(s string) ([]byte, error) { 87 | return base64.RawURLEncoding.DecodeString(s) 88 | } 89 | 90 | func serialize(v interface{}) (string, error) { 91 | j, err := json.Marshal(v) 92 | if err != nil { 93 | return "", err 94 | } 95 | return encodeToString(j), nil 96 | } 97 | 98 | func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims) (string, error) { 99 | if header == nil { 100 | return "", errors.New("header is required") 101 | } 102 | 103 | if kp == nil { 104 | return "", errors.New("keypair is required") 105 | } 106 | 107 | if c.Subject == "" { 108 | return "", errors.New("subject is not set") 109 | } 110 | 111 | h, err := serialize(header) 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | issuerBytes, err := kp.PublicKey() 117 | if err != nil { 118 | return "", err 119 | } 120 | 121 | prefixes := claim.ExpectedPrefixes() 122 | if prefixes != nil { 123 | ok := false 124 | for _, p := range prefixes { 125 | switch p { 126 | case nkeys.PrefixByteAccount: 127 | if nkeys.IsValidPublicAccountKey(issuerBytes) { 128 | ok = true 129 | } 130 | case nkeys.PrefixByteOperator: 131 | if nkeys.IsValidPublicOperatorKey(issuerBytes) { 132 | ok = true 133 | } 134 | case nkeys.PrefixByteServer: 135 | if nkeys.IsValidPublicServerKey(issuerBytes) { 136 | ok = true 137 | } 138 | case nkeys.PrefixByteCluster: 139 | if nkeys.IsValidPublicClusterKey(issuerBytes) { 140 | ok = true 141 | } 142 | case nkeys.PrefixByteUser: 143 | if nkeys.IsValidPublicUserKey(issuerBytes) { 144 | ok = true 145 | } 146 | } 147 | } 148 | if !ok { 149 | return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes) 150 | } 151 | } 152 | 153 | c.Issuer = string(issuerBytes) 154 | c.IssuedAt = time.Now().UTC().Unix() 155 | 156 | c.ID, err = c.hash() 157 | if err != nil { 158 | return "", err 159 | } 160 | 161 | payload, err := serialize(claim) 162 | if err != nil { 163 | return "", err 164 | } 165 | 166 | sig, err := kp.Sign([]byte(payload)) 167 | if err != nil { 168 | return "", err 169 | } 170 | eSig := encodeToString(sig) 171 | return fmt.Sprintf("%s.%s.%s", h, payload, eSig), nil 172 | } 173 | 174 | func (c *ClaimsData) hash() (string, error) { 175 | j, err := json.Marshal(c) 176 | if err != nil { 177 | return "", err 178 | } 179 | h := sha512.New512_256() 180 | h.Write(j) 181 | return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil 182 | } 183 | 184 | // Encode encodes a claim into a JWT token. The claim is signed with the 185 | // provided nkey's private key 186 | func (c *ClaimsData) Encode(kp nkeys.KeyPair, payload Claims) (string, error) { 187 | return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload) 188 | } 189 | 190 | // Returns a JSON representation of the claim 191 | func (c *ClaimsData) String(claim interface{}) string { 192 | j, err := json.MarshalIndent(claim, "", " ") 193 | if err != nil { 194 | return "" 195 | } 196 | return string(j) 197 | } 198 | 199 | func parseClaims(s string, target Claims) error { 200 | h, err := decodeString(s) 201 | if err != nil { 202 | return err 203 | } 204 | return json.Unmarshal(h, &target) 205 | } 206 | 207 | // Verify verifies that the encoded payload was signed by the 208 | // provided public key. Verify is called automatically with 209 | // the claims portion of the token and the public key in the claim. 210 | // Client code need to insure that the public key in the 211 | // claim is trusted. 212 | func (c *ClaimsData) Verify(payload string, sig []byte) bool { 213 | // decode the public key 214 | kp, err := nkeys.FromPublicKey(c.Issuer) 215 | if err != nil { 216 | return false 217 | } 218 | if err := kp.Verify([]byte(payload), sig); err != nil { 219 | return false 220 | } 221 | return true 222 | } 223 | 224 | // Validate checks a claim to make sure it is valid. Validity checks 225 | // include expiration and not before constraints. 226 | func (c *ClaimsData) Validate(vr *ValidationResults) { 227 | now := time.Now().UTC().Unix() 228 | if c.Expires > 0 && now > c.Expires { 229 | vr.AddTimeCheck("claim is expired") 230 | } 231 | 232 | if c.NotBefore > 0 && c.NotBefore > now { 233 | vr.AddTimeCheck("claim is not yet valid") 234 | } 235 | } 236 | 237 | // IsSelfSigned returns true if the claims issuer is the subject 238 | func (c *ClaimsData) IsSelfSigned() bool { 239 | return c.Issuer == c.Subject 240 | } 241 | 242 | // Decode takes a JWT string decodes it and validates it 243 | // and return the embedded Claims. If the token header 244 | // doesn't match the expected algorithm, or the claim is 245 | // not valid or verification fails an error is returned. 246 | func Decode(token string, target Claims) error { 247 | // must have 3 chunks 248 | chunks := strings.Split(token, ".") 249 | if len(chunks) != 3 { 250 | return errors.New("expected 3 chunks") 251 | } 252 | 253 | _, err := parseHeaders(chunks[0]) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | if err := parseClaims(chunks[1], target); err != nil { 259 | return err 260 | } 261 | 262 | sig, err := decodeString(chunks[2]) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | if !target.Verify(chunks[1], sig) { 268 | return errors.New("claim failed signature verification") 269 | } 270 | 271 | prefixes := target.ExpectedPrefixes() 272 | if prefixes != nil { 273 | ok := false 274 | issuer := target.Claims().Issuer 275 | for _, p := range prefixes { 276 | switch p { 277 | case nkeys.PrefixByteAccount: 278 | if nkeys.IsValidPublicAccountKey(issuer) { 279 | ok = true 280 | } 281 | case nkeys.PrefixByteOperator: 282 | if nkeys.IsValidPublicOperatorKey(issuer) { 283 | ok = true 284 | } 285 | case nkeys.PrefixByteServer: 286 | if nkeys.IsValidPublicServerKey(issuer) { 287 | ok = true 288 | } 289 | case nkeys.PrefixByteCluster: 290 | if nkeys.IsValidPublicClusterKey(issuer) { 291 | ok = true 292 | } 293 | case nkeys.PrefixByteUser: 294 | if nkeys.IsValidPublicUserKey(issuer) { 295 | ok = true 296 | } 297 | } 298 | } 299 | if !ok { 300 | return fmt.Errorf("unable to validate expected prefixes - %v", prefixes) 301 | } 302 | } 303 | 304 | return nil 305 | } 306 | -------------------------------------------------------------------------------- /v2/v1compat/cluster_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | 21 | "github.com/nats-io/nkeys" 22 | ) 23 | 24 | // Cluster stores the cluster specific elements of a cluster JWT 25 | // Deprecated: ClusterClaims are not supported 26 | type Cluster struct { 27 | Trust []string `json:"identity,omitempty"` 28 | Accounts []string `json:"accts,omitempty"` 29 | AccountURL string `json:"accturl,omitempty"` 30 | OperatorURL string `json:"opurl,omitempty"` 31 | } 32 | 33 | // Validate checks the cluster and permissions for a cluster JWT 34 | func (c *Cluster) Validate(vr *ValidationResults) { 35 | // fixme validate cluster data 36 | } 37 | 38 | // ClusterClaims defines the data in a cluster JWT 39 | // Deprecated: ClusterClaims are not supported 40 | type ClusterClaims struct { 41 | ClaimsData 42 | Cluster `json:"nats,omitempty"` 43 | } 44 | 45 | // NewClusterClaims creates a new cluster JWT with the specified subject/public key 46 | // Deprecated: ClusterClaims are not supported 47 | func NewClusterClaims(subject string) *ClusterClaims { 48 | if subject == "" { 49 | return nil 50 | } 51 | c := &ClusterClaims{} 52 | c.Subject = subject 53 | return c 54 | } 55 | 56 | // Encode tries to turn the cluster claims into a JWT string 57 | func (c *ClusterClaims) Encode(pair nkeys.KeyPair) (string, error) { 58 | if !nkeys.IsValidPublicClusterKey(c.Subject) { 59 | return "", errors.New("expected subject to be a cluster public key") 60 | } 61 | c.ClaimsData.Type = ClusterClaim 62 | return c.ClaimsData.Encode(pair, c) 63 | } 64 | 65 | // DecodeClusterClaims tries to parse cluster claims from a JWT string 66 | // Deprecated: ClusterClaims are not supported 67 | func DecodeClusterClaims(token string) (*ClusterClaims, error) { 68 | v := ClusterClaims{} 69 | if err := Decode(token, &v); err != nil { 70 | return nil, err 71 | } 72 | return &v, nil 73 | } 74 | 75 | func (c *ClusterClaims) String() string { 76 | return c.ClaimsData.String(c) 77 | } 78 | 79 | // Payload returns the cluster specific data 80 | func (c *ClusterClaims) Payload() interface{} { 81 | return &c.Cluster 82 | } 83 | 84 | // Validate checks the generic and cluster data in the cluster claims 85 | func (c *ClusterClaims) Validate(vr *ValidationResults) { 86 | c.ClaimsData.Validate(vr) 87 | c.Cluster.Validate(vr) 88 | } 89 | 90 | // ExpectedPrefixes defines the types that can encode a cluster JWT, operator or cluster 91 | func (c *ClusterClaims) ExpectedPrefixes() []nkeys.PrefixByte { 92 | return []nkeys.PrefixByte{nkeys.PrefixByteOperator, nkeys.PrefixByteCluster} 93 | } 94 | 95 | // Claims returns the generic data 96 | func (c *ClusterClaims) Claims() *ClaimsData { 97 | return &c.ClaimsData 98 | } 99 | -------------------------------------------------------------------------------- /v2/v1compat/cluster_claims_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | "github.com/nats-io/nkeys" 23 | ) 24 | 25 | func TestNewClusterClaims(t *testing.T) { 26 | ckp := createClusterNKey(t) 27 | skp := createClusterNKey(t) 28 | 29 | uc := NewClusterClaims(publicKey(skp, t)) 30 | uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() 31 | uJwt := encode(uc, ckp, t) 32 | 33 | uc2, err := DecodeClusterClaims(uJwt) 34 | if err != nil { 35 | t.Fatal("failed to decode", err) 36 | } 37 | 38 | AssertEquals(uc.String(), uc2.String(), t) 39 | 40 | AssertEquals(uc.Claims() != nil, true, t) 41 | AssertEquals(uc.Payload() != nil, true, t) 42 | } 43 | 44 | func TestClusterClaimsIssuer(t *testing.T) { 45 | ckp := createClusterNKey(t) 46 | skp := createClusterNKey(t) 47 | 48 | uc := NewClusterClaims(publicKey(skp, t)) 49 | uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() 50 | uJwt := encode(uc, ckp, t) 51 | 52 | temp, err := DecodeGeneric(uJwt) 53 | if err != nil { 54 | t.Fatal("failed to decode", err) 55 | } 56 | 57 | type kpInputs struct { 58 | name string 59 | kp nkeys.KeyPair 60 | ok bool 61 | } 62 | 63 | inputs := []kpInputs{ 64 | {"account", createAccountNKey(t), false}, 65 | {"user", createUserNKey(t), false}, 66 | {"operator", createOperatorNKey(t), true}, 67 | {"server", createServerNKey(t), false}, 68 | {"cluster", createClusterNKey(t), true}, 69 | } 70 | 71 | for _, i := range inputs { 72 | bad := encode(temp, i.kp, t) 73 | _, err = DecodeClusterClaims(bad) 74 | if i.ok && err != nil { 75 | t.Fatalf("unexpected error for %q: %v", i.name, err) 76 | } 77 | if !i.ok && err == nil { 78 | t.Logf("should have failed to decode cluster signed by %q", i.name) 79 | t.Fail() 80 | } 81 | } 82 | } 83 | 84 | func TestClusterSubjects(t *testing.T) { 85 | type kpInputs struct { 86 | name string 87 | kp nkeys.KeyPair 88 | ok bool 89 | } 90 | 91 | inputs := []kpInputs{ 92 | {"account", createAccountNKey(t), false}, 93 | {"server", createServerNKey(t), false}, 94 | {"operator", createOperatorNKey(t), false}, 95 | {"cluster", createClusterNKey(t), true}, 96 | {"user", createUserNKey(t), false}, 97 | } 98 | 99 | for _, i := range inputs { 100 | c := NewClusterClaims(publicKey(i.kp, t)) 101 | _, err := c.Encode(createOperatorNKey(t)) 102 | if i.ok && err != nil { 103 | t.Fatalf("unexpected error for %q: %v", i.name, err) 104 | } 105 | if !i.ok && err == nil { 106 | t.Logf("should have failed to encode cluster with with %q subject", i.name) 107 | t.Fail() 108 | } 109 | } 110 | } 111 | 112 | func TestNewNilClusterClaims(t *testing.T) { 113 | v := NewClusterClaims("") 114 | if v != nil { 115 | t.Fatal("expected nil user claim") 116 | } 117 | } 118 | 119 | func TestClusterType(t *testing.T) { 120 | c := NewClusterClaims(publicKey(createClusterNKey(t), t)) 121 | s := encode(c, createClusterNKey(t), t) 122 | u, err := DecodeClusterClaims(s) 123 | if err != nil { 124 | t.Fatalf("failed to decode cluster claim: %v", err) 125 | } 126 | 127 | if ClusterClaim != u.Type { 128 | t.Fatalf("type is unexpected %q (wanted cluster)", u.Type) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /v2/v1compat/creds_utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "regexp" 23 | "strings" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | // DecorateJWT returns a decorated JWT that describes the kind of JWT 29 | func DecorateJWT(jwtString string) ([]byte, error) { 30 | gc, err := DecodeGeneric(jwtString) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return formatJwt(string(gc.Type), jwtString) 35 | } 36 | 37 | func formatJwt(kind string, jwtString string) ([]byte, error) { 38 | templ := `-----BEGIN NATS %s JWT----- 39 | %s 40 | ------END NATS %s JWT------ 41 | 42 | ` 43 | w := bytes.NewBuffer(nil) 44 | kind = strings.ToUpper(kind) 45 | _, err := fmt.Fprintf(w, templ, kind, jwtString, kind) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return w.Bytes(), nil 50 | } 51 | 52 | // DecorateSeed takes a seed and returns a string that wraps 53 | // the seed in the form: 54 | // 55 | // ************************* IMPORTANT ************************* 56 | // NKEY Seed printed below can be used sign and prove identity. 57 | // NKEYs are sensitive and should be treated as secrets. 58 | // 59 | // -----BEGIN USER NKEY SEED----- 60 | // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM 61 | // ------END USER NKEY SEED------ 62 | func DecorateSeed(seed []byte) ([]byte, error) { 63 | w := bytes.NewBuffer(nil) 64 | ts := bytes.TrimSpace(seed) 65 | pre := string(ts[0:2]) 66 | kind := "" 67 | switch pre { 68 | case "SU": 69 | kind = "USER" 70 | case "SA": 71 | kind = "ACCOUNT" 72 | case "SO": 73 | kind = "OPERATOR" 74 | default: 75 | return nil, errors.New("seed is not an operator, account or user seed") 76 | } 77 | header := `************************* IMPORTANT ************************* 78 | NKEY Seed printed below can be used to sign and prove identity. 79 | NKEYs are sensitive and should be treated as secrets. 80 | 81 | -----BEGIN %s NKEY SEED----- 82 | ` 83 | _, err := fmt.Fprintf(w, header, kind) 84 | if err != nil { 85 | return nil, err 86 | } 87 | w.Write(ts) 88 | 89 | footer := ` 90 | ------END %s NKEY SEED------ 91 | 92 | ************************************************************* 93 | ` 94 | _, err = fmt.Fprintf(w, footer, kind) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return w.Bytes(), nil 99 | } 100 | 101 | var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`) 102 | 103 | // An user config file looks like this: 104 | // -----BEGIN NATS USER JWT----- 105 | // eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5... 106 | // ------END NATS USER JWT------ 107 | // 108 | // ************************* IMPORTANT ************************* 109 | // NKEY Seed printed below can be used sign and prove identity. 110 | // NKEYs are sensitive and should be treated as secrets. 111 | // 112 | // -----BEGIN USER NKEY SEED----- 113 | // SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM 114 | // ------END USER NKEY SEED------ 115 | 116 | // FormatUserConfig returns a decorated file with a decorated JWT and decorated seed 117 | func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) { 118 | gc, err := DecodeGeneric(jwtString) 119 | if err != nil { 120 | return nil, err 121 | } 122 | if gc.Type != UserClaim { 123 | return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.Type)) 124 | } 125 | 126 | w := bytes.NewBuffer(nil) 127 | 128 | jd, err := formatJwt(string(gc.Type), jwtString) 129 | if err != nil { 130 | return nil, err 131 | } 132 | _, err = w.Write(jd) 133 | if err != nil { 134 | return nil, err 135 | } 136 | if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) { 137 | return nil, fmt.Errorf("nkey seed is not an user seed") 138 | } 139 | 140 | d, err := DecorateSeed(seed) 141 | if err != nil { 142 | return nil, err 143 | } 144 | _, err = w.Write(d) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | return w.Bytes(), nil 150 | } 151 | 152 | // ParseDecoratedJWT takes a creds file and returns the JWT portion. 153 | func ParseDecoratedJWT(contents []byte) (string, error) { 154 | items := userConfigRE.FindAllSubmatch(contents, -1) 155 | if len(items) == 0 { 156 | return string(contents), nil 157 | } 158 | // First result should be the user JWT. 159 | // We copy here so that if the file contained a seed file too we wipe appropriately. 160 | raw := items[0][1] 161 | tmp := make([]byte, len(raw)) 162 | copy(tmp, raw) 163 | return string(tmp), nil 164 | } 165 | 166 | // ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a 167 | // key pair from it. 168 | func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) { 169 | var seed []byte 170 | 171 | items := userConfigRE.FindAllSubmatch(contents, -1) 172 | if len(items) > 1 { 173 | seed = items[1][1] 174 | } else { 175 | lines := bytes.Split(contents, []byte("\n")) 176 | for _, line := range lines { 177 | if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) || 178 | bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) || 179 | bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) { 180 | seed = line 181 | break 182 | } 183 | } 184 | } 185 | if seed == nil { 186 | return nil, errors.New("no nkey seed found") 187 | } 188 | if !bytes.HasPrefix(seed, []byte("SO")) && 189 | !bytes.HasPrefix(seed, []byte("SA")) && 190 | !bytes.HasPrefix(seed, []byte("SU")) { 191 | return nil, errors.New("doesn't contain a seed nkey") 192 | } 193 | kp, err := nkeys.FromSeed(seed) 194 | if err != nil { 195 | return nil, err 196 | } 197 | return kp, nil 198 | } 199 | 200 | // ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a 201 | // key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys. 202 | func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) { 203 | nk, err := ParseDecoratedNKey(contents) 204 | if err != nil { 205 | return nil, err 206 | } 207 | seed, err := nk.Seed() 208 | if err != nil { 209 | return nil, err 210 | } 211 | if !bytes.HasPrefix(seed, []byte("SU")) { 212 | return nil, errors.New("doesn't contain an user seed nkey") 213 | } 214 | kp, err := nkeys.FromSeed(seed) 215 | if err != nil { 216 | return nil, err 217 | } 218 | return kp, nil 219 | } 220 | -------------------------------------------------------------------------------- /v2/v1compat/creds_utils_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "bytes" 20 | "fmt" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/nats-io/nkeys" 25 | ) 26 | 27 | func makeJWT(t *testing.T) (string, nkeys.KeyPair) { 28 | akp := createAccountNKey(t) 29 | kp := createUserNKey(t) 30 | pk := publicKey(kp, t) 31 | oc := NewUserClaims(pk) 32 | token, err := oc.Encode(akp) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | return token, kp 37 | } 38 | 39 | func Test_DecorateJwt(t *testing.T) { 40 | token, _ := makeJWT(t) 41 | d, err := DecorateJWT(token) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | s := string(d) 46 | if !strings.Contains(s, "-BEGIN NATS USER JWT-") { 47 | t.Fatal("doesn't contain expected header") 48 | } 49 | if !strings.Contains(s, "eyJ0") { 50 | t.Fatal("doesn't contain public key") 51 | } 52 | if !strings.Contains(s, "-END NATS USER JWT------\n\n") { 53 | t.Fatal("doesn't contain expected footer") 54 | } 55 | } 56 | 57 | func Test_FormatUserConfig(t *testing.T) { 58 | token, kp := makeJWT(t) 59 | d, err := FormatUserConfig(token, seedKey(kp, t)) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | s := string(d) 64 | if !strings.Contains(s, "-BEGIN NATS USER JWT-") { 65 | t.Fatal("doesn't contain expected header") 66 | } 67 | if !strings.Contains(s, "eyJ0") { 68 | t.Fatal("doesn't contain public key") 69 | } 70 | if !strings.Contains(s, "-END NATS USER JWT-") { 71 | t.Fatal("doesn't contain expected footer") 72 | } 73 | 74 | validateSeed(t, d, kp) 75 | } 76 | 77 | func validateSeed(t *testing.T, decorated []byte, nk nkeys.KeyPair) { 78 | kind := "" 79 | seed := seedKey(nk, t) 80 | switch string(seed[0:2]) { 81 | case "SO": 82 | kind = "operator" 83 | case "SA": 84 | kind = "account" 85 | case "SU": 86 | kind = "user" 87 | default: 88 | kind = "not supported" 89 | } 90 | kind = strings.ToUpper(kind) 91 | 92 | s := string(decorated) 93 | if !strings.Contains(s, fmt.Sprintf("\n\n-----BEGIN %s NKEY SEED-", kind)) { 94 | t.Fatal("doesn't contain expected seed header") 95 | } 96 | if !strings.Contains(s, string(seed)) { 97 | t.Fatal("doesn't contain the seed") 98 | } 99 | if !strings.Contains(s, fmt.Sprintf("-END %s NKEY SEED------\n\n", kind)) { 100 | t.Fatal("doesn't contain expected seed footer") 101 | } 102 | } 103 | 104 | func Test_ParseDecoratedJWT(t *testing.T) { 105 | token, _ := makeJWT(t) 106 | 107 | t2, err := ParseDecoratedJWT([]byte(token)) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | if token != t2 { 112 | t.Fatal("jwt didn't match expected") 113 | } 114 | 115 | decorated, err := DecorateJWT(token) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | t3, err := ParseDecoratedJWT(decorated) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | if token != t3 { 125 | t.Fatal("parse decorated jwt didn't match expected") 126 | } 127 | } 128 | 129 | func Test_ParseDecoratedJWTBad(t *testing.T) { 130 | v, err := ParseDecoratedJWT([]byte("foo")) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | if v != "foo" { 135 | t.Fatal("unexpected input was not returned") 136 | } 137 | } 138 | 139 | func Test_ParseDecoratedSeed(t *testing.T) { 140 | token, ukp := makeJWT(t) 141 | us := seedKey(ukp, t) 142 | decorated, err := FormatUserConfig(token, us) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | kp, err := ParseDecoratedUserNKey(decorated) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | pu := seedKey(kp, t) 151 | if !bytes.Equal(us, pu) { 152 | t.Fatal("seeds don't match") 153 | } 154 | } 155 | 156 | func Test_ParseDecoratedBadKey(t *testing.T) { 157 | token, ukp := makeJWT(t) 158 | us, err := ukp.Seed() 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | akp := createAccountNKey(t) 163 | as := seedKey(akp, t) 164 | 165 | _, err = FormatUserConfig(token, as) 166 | if err == nil { 167 | t.Fatal("should have failed to encode with bad seed") 168 | } 169 | 170 | sc, err := FormatUserConfig(token, us) 171 | if err != nil { 172 | t.Fatal(err) 173 | } 174 | bad := strings.Replace(string(sc), string(us), string(as), -1) 175 | _, err = ParseDecoratedUserNKey([]byte(bad)) 176 | if err == nil { 177 | t.Fatal("parse should have failed for non user nkey") 178 | } 179 | } 180 | 181 | func Test_FailsOnNonUserJWT(t *testing.T) { 182 | akp := createAccountNKey(t) 183 | pk := publicKey(akp, t) 184 | 185 | ac := NewAccountClaims(pk) 186 | token, err := ac.Encode(akp) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | ukp := createUserNKey(t) 191 | us := seedKey(ukp, t) 192 | _, err = FormatUserConfig(token, us) 193 | if err == nil { 194 | t.Fatal("should have failed with account claims") 195 | } 196 | } 197 | 198 | func Test_DecorateNKeys(t *testing.T) { 199 | var kps []nkeys.KeyPair 200 | kps = append(kps, createOperatorNKey(t)) 201 | kps = append(kps, createAccountNKey(t)) 202 | kps = append(kps, createUserNKey(t)) 203 | 204 | for _, kp := range kps { 205 | seed := seedKey(kp, t) 206 | d, err := DecorateSeed(seed) 207 | if err != nil { 208 | t.Fatal(err, string(seed)) 209 | } 210 | validateSeed(t, d, kp) 211 | 212 | kp2, err := ParseDecoratedNKey(d) 213 | if err != nil { 214 | t.Fatal(string(seed), err) 215 | } 216 | seed2 := seedKey(kp2, t) 217 | if !bytes.Equal(seed, seed2) { 218 | t.Fatalf("seeds dont match %q != %q", string(seed), string(seed2)) 219 | } 220 | } 221 | 222 | _, err := ParseDecoratedNKey([]byte("bad")) 223 | if err == nil { 224 | t.Fatal("required error parsing bad nkey") 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /v2/v1compat/exports.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2019 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "fmt" 20 | "time" 21 | ) 22 | 23 | // ResponseType is used to store an export response type 24 | type ResponseType string 25 | 26 | const ( 27 | // ResponseTypeSingleton is used for a service that sends a single response only 28 | ResponseTypeSingleton = "Singleton" 29 | 30 | // ResponseTypeStream is used for a service that will send multiple responses 31 | ResponseTypeStream = "Stream" 32 | 33 | // ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream) 34 | ResponseTypeChunked = "Chunked" 35 | ) 36 | 37 | // ServiceLatency is used when observing and exported service for 38 | // latency measurements. 39 | // Sampling 1-100, represents sampling rate, defaults to 100. 40 | // Results is the subject where the latency metrics are published. 41 | // A metric will be defined by the nats-server's ServiceLatency. Time durations 42 | // are in nanoseconds. 43 | // see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524 44 | // e.g. 45 | // 46 | // { 47 | // "app": "dlc22", 48 | // "start": "2019-09-16T21:46:23.636869585-07:00", 49 | // "svc": 219732, 50 | // "nats": { 51 | // "req": 320415, 52 | // "resp": 228268, 53 | // "sys": 0 54 | // }, 55 | // "total": 768415 56 | // } 57 | type ServiceLatency struct { 58 | Sampling int `json:"sampling,omitempty"` 59 | Results Subject `json:"results"` 60 | } 61 | 62 | func (sl *ServiceLatency) Validate(vr *ValidationResults) { 63 | if sl.Sampling < 1 || sl.Sampling > 100 { 64 | vr.AddError("sampling percentage needs to be between 1-100") 65 | } 66 | sl.Results.Validate(vr) 67 | if sl.Results.HasWildCards() { 68 | vr.AddError("results subject can not contain wildcards") 69 | } 70 | } 71 | 72 | // Export represents a single export 73 | type Export struct { 74 | Name string `json:"name,omitempty"` 75 | Subject Subject `json:"subject,omitempty"` 76 | Type ExportType `json:"type,omitempty"` 77 | TokenReq bool `json:"token_req,omitempty"` 78 | Revocations RevocationList `json:"revocations,omitempty"` 79 | ResponseType ResponseType `json:"response_type,omitempty"` 80 | Latency *ServiceLatency `json:"service_latency,omitempty"` 81 | AccountTokenPosition uint `json:"account_token_position,omitempty"` 82 | } 83 | 84 | // IsService returns true if an export is for a service 85 | func (e *Export) IsService() bool { 86 | return e.Type == Service 87 | } 88 | 89 | // IsStream returns true if an export is for a stream 90 | func (e *Export) IsStream() bool { 91 | return e.Type == Stream 92 | } 93 | 94 | // IsSingleResponse returns true if an export has a single response 95 | // or no resopnse type is set, also checks that the type is service 96 | func (e *Export) IsSingleResponse() bool { 97 | return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "") 98 | } 99 | 100 | // IsChunkedResponse returns true if an export has a chunked response 101 | func (e *Export) IsChunkedResponse() bool { 102 | return e.Type == Service && e.ResponseType == ResponseTypeChunked 103 | } 104 | 105 | // IsStreamResponse returns true if an export has a chunked response 106 | func (e *Export) IsStreamResponse() bool { 107 | return e.Type == Service && e.ResponseType == ResponseTypeStream 108 | } 109 | 110 | // Validate appends validation issues to the passed in results list 111 | func (e *Export) Validate(vr *ValidationResults) { 112 | if e == nil { 113 | vr.AddError("null export is not allowed") 114 | return 115 | } 116 | if !e.IsService() && !e.IsStream() { 117 | vr.AddError("invalid export type: %q", e.Type) 118 | } 119 | if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() { 120 | vr.AddError("invalid response type for service: %q", e.ResponseType) 121 | } 122 | if e.IsStream() && e.ResponseType != "" { 123 | vr.AddError("invalid response type for stream: %q", e.ResponseType) 124 | } 125 | if e.Latency != nil { 126 | if !e.IsService() { 127 | vr.AddError("latency tracking only permitted for services") 128 | } 129 | e.Latency.Validate(vr) 130 | } 131 | e.Subject.Validate(vr) 132 | } 133 | 134 | // Revoke enters a revocation by publickey using time.Now(). 135 | func (e *Export) Revoke(pubKey string) { 136 | e.RevokeAt(pubKey, time.Now()) 137 | } 138 | 139 | // RevokeAt enters a revocation by publickey and timestamp into this export 140 | // If there is already a revocation for this public key that is newer, it is kept. 141 | func (e *Export) RevokeAt(pubKey string, timestamp time.Time) { 142 | if e.Revocations == nil { 143 | e.Revocations = RevocationList{} 144 | } 145 | 146 | e.Revocations.Revoke(pubKey, timestamp) 147 | } 148 | 149 | // ClearRevocation removes any revocation for the public key 150 | func (e *Export) ClearRevocation(pubKey string) { 151 | e.Revocations.ClearRevocation(pubKey) 152 | } 153 | 154 | // IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in. 155 | // Generally this method is called with the subject and issue time of the jwt to be tested. 156 | // DO NOT pass time.Now(), it will not produce a stable/expected response. 157 | func (e *Export) IsRevokedAt(pubKey string, timestamp time.Time) bool { 158 | return e.Revocations.IsRevoked(pubKey, timestamp) 159 | } 160 | 161 | // IsRevoked does not perform a valid check. Use IsRevokedAt instead. 162 | func (e *Export) IsRevoked(_ string) bool { 163 | return true 164 | } 165 | 166 | // Exports is a slice of exports 167 | type Exports []*Export 168 | 169 | // Add appends exports to the list 170 | func (e *Exports) Add(i ...*Export) { 171 | *e = append(*e, i...) 172 | } 173 | 174 | func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { 175 | m := make(map[string]string) 176 | for i, ns := range subjects { 177 | for j, s := range subjects { 178 | if i == j { 179 | continue 180 | } 181 | if ns.IsContainedIn(s) { 182 | str := string(s) 183 | _, ok := m[str] 184 | if !ok { 185 | m[str] = string(ns) 186 | } 187 | } 188 | } 189 | } 190 | 191 | if len(m) != 0 { 192 | for k, v := range m { 193 | var vi ValidationIssue 194 | vi.Blocking = true 195 | vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v) 196 | vr.Add(&vi) 197 | } 198 | } 199 | } 200 | 201 | // Validate calls validate on all of the exports 202 | func (e *Exports) Validate(vr *ValidationResults) error { 203 | var serviceSubjects []Subject 204 | var streamSubjects []Subject 205 | 206 | for _, v := range *e { 207 | if v == nil { 208 | vr.AddError("null export is not allowed") 209 | continue 210 | } 211 | if v.IsService() { 212 | serviceSubjects = append(serviceSubjects, v.Subject) 213 | } else { 214 | streamSubjects = append(streamSubjects, v.Subject) 215 | } 216 | v.Validate(vr) 217 | } 218 | 219 | isContainedIn(Service, serviceSubjects, vr) 220 | isContainedIn(Stream, streamSubjects, vr) 221 | 222 | return nil 223 | } 224 | 225 | // HasExportContainingSubject checks if the export list has an export with the provided subject 226 | func (e *Exports) HasExportContainingSubject(subject Subject) bool { 227 | for _, s := range *e { 228 | if subject.IsContainedIn(s.Subject) { 229 | return true 230 | } 231 | } 232 | return false 233 | } 234 | 235 | func (e Exports) Len() int { 236 | return len(e) 237 | } 238 | 239 | func (e Exports) Swap(i, j int) { 240 | e[i], e[j] = e[j], e[i] 241 | } 242 | 243 | func (e Exports) Less(i, j int) bool { 244 | return e[i].Subject < e[j].Subject 245 | } 246 | -------------------------------------------------------------------------------- /v2/v1compat/exports_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "sort" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestSimpleExportValidation(t *testing.T) { 25 | e := &Export{Subject: "foo", Type: Stream} 26 | 27 | vr := CreateValidationResults() 28 | e.Validate(vr) 29 | 30 | if !vr.IsEmpty() { 31 | t.Errorf("simple export should validate cleanly") 32 | } 33 | 34 | e.Type = Service 35 | vr = CreateValidationResults() 36 | e.Validate(vr) 37 | 38 | if !vr.IsEmpty() { 39 | t.Errorf("simple export should validate cleanly") 40 | } 41 | } 42 | 43 | func TestResponseTypeValidation(t *testing.T) { 44 | e := &Export{Subject: "foo", Type: Stream, ResponseType: ResponseTypeSingleton} 45 | 46 | vr := CreateValidationResults() 47 | e.Validate(vr) 48 | 49 | if vr.IsEmpty() { 50 | t.Errorf("response type on stream should have an validation issue") 51 | } 52 | if e.IsSingleResponse() { 53 | t.Errorf("response type should always fail for stream") 54 | } 55 | 56 | e.Type = Service 57 | vr = CreateValidationResults() 58 | e.Validate(vr) 59 | if !vr.IsEmpty() { 60 | t.Errorf("response type on service should validate cleanly") 61 | } 62 | if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { 63 | t.Errorf("response type should be single") 64 | } 65 | 66 | e.ResponseType = ResponseTypeChunked 67 | vr = CreateValidationResults() 68 | e.Validate(vr) 69 | if !vr.IsEmpty() { 70 | t.Errorf("response type on service should validate cleanly") 71 | } 72 | if e.IsSingleResponse() || !e.IsChunkedResponse() || e.IsStreamResponse() { 73 | t.Errorf("response type should be chunk") 74 | } 75 | 76 | e.ResponseType = ResponseTypeStream 77 | vr = CreateValidationResults() 78 | e.Validate(vr) 79 | if !vr.IsEmpty() { 80 | t.Errorf("response type on service should validate cleanly") 81 | } 82 | if e.IsSingleResponse() || e.IsChunkedResponse() || !e.IsStreamResponse() { 83 | t.Errorf("response type should be stream") 84 | } 85 | 86 | e.ResponseType = "" 87 | vr = CreateValidationResults() 88 | e.Validate(vr) 89 | if !vr.IsEmpty() { 90 | t.Errorf("response type on service should validate cleanly") 91 | } 92 | if !e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { 93 | t.Errorf("response type should be single") 94 | } 95 | 96 | e.ResponseType = "bad" 97 | vr = CreateValidationResults() 98 | e.Validate(vr) 99 | if vr.IsEmpty() { 100 | t.Errorf("response type should match available options") 101 | } 102 | if e.IsSingleResponse() || e.IsChunkedResponse() || e.IsStreamResponse() { 103 | t.Errorf("response type should be bad") 104 | } 105 | } 106 | 107 | func TestInvalidExportType(t *testing.T) { 108 | i := &Export{Subject: "foo", Type: Unknown} 109 | 110 | vr := CreateValidationResults() 111 | i.Validate(vr) 112 | 113 | if vr.IsEmpty() { 114 | t.Errorf("export with bad type should not validate cleanly") 115 | } 116 | 117 | if !vr.IsBlocking(true) { 118 | t.Errorf("invalid type is blocking") 119 | } 120 | } 121 | 122 | func TestOverlappingExports(t *testing.T) { 123 | i := &Export{Subject: "bar.foo", Type: Stream} 124 | i2 := &Export{Subject: "bar.*", Type: Stream} 125 | 126 | exports := &Exports{} 127 | exports.Add(i, i2) 128 | 129 | vr := CreateValidationResults() 130 | exports.Validate(vr) 131 | 132 | if len(vr.Issues) != 1 { 133 | t.Errorf("export has overlapping subjects") 134 | } 135 | } 136 | 137 | func TestDifferentExportTypes_OverlapOK(t *testing.T) { 138 | i := &Export{Subject: "bar.foo", Type: Service} 139 | i2 := &Export{Subject: "bar.*", Type: Stream} 140 | 141 | exports := &Exports{} 142 | exports.Add(i, i2) 143 | 144 | vr := CreateValidationResults() 145 | exports.Validate(vr) 146 | 147 | if len(vr.Issues) != 0 { 148 | t.Errorf("should allow overlaps on different export kind") 149 | } 150 | } 151 | 152 | func TestDifferentExportTypes_SameSubjectOK(t *testing.T) { 153 | i := &Export{Subject: "bar", Type: Service} 154 | i2 := &Export{Subject: "bar", Type: Stream} 155 | 156 | exports := &Exports{} 157 | exports.Add(i, i2) 158 | 159 | vr := CreateValidationResults() 160 | exports.Validate(vr) 161 | 162 | if len(vr.Issues) != 0 { 163 | t.Errorf("should allow overlaps on different export kind") 164 | } 165 | } 166 | 167 | func TestSameExportType_SameSubject(t *testing.T) { 168 | i := &Export{Subject: "bar", Type: Service} 169 | i2 := &Export{Subject: "bar", Type: Service} 170 | 171 | exports := &Exports{} 172 | exports.Add(i, i2) 173 | 174 | vr := CreateValidationResults() 175 | exports.Validate(vr) 176 | 177 | if len(vr.Issues) != 1 { 178 | t.Errorf("should not allow same subject on same export kind") 179 | } 180 | } 181 | 182 | func TestExportRevocation(t *testing.T) { 183 | akp := createAccountNKey(t) 184 | apk := publicKey(akp, t) 185 | account := NewAccountClaims(apk) 186 | e := &Export{Subject: "foo", Type: Stream} 187 | 188 | account.Exports.Add(e) 189 | 190 | pubKey := "bar" 191 | now := time.Now() 192 | 193 | // test that clear is safe before we add any 194 | e.ClearRevocation(pubKey) 195 | 196 | if e.IsRevokedAt(pubKey, now) { 197 | t.Errorf("no revocation was added so is revoked should be false") 198 | } 199 | 200 | e.RevokeAt(pubKey, now.Add(time.Second*100)) 201 | 202 | if !e.IsRevokedAt(pubKey, now) { 203 | t.Errorf("revocation should hold when timestamp is in the future") 204 | } 205 | 206 | if e.IsRevokedAt(pubKey, now.Add(time.Second*150)) { 207 | t.Errorf("revocation should time out") 208 | } 209 | 210 | e.RevokeAt(pubKey, now.Add(time.Second*50)) // shouldn't change the revocation, you can't move it in 211 | 212 | if !e.IsRevokedAt(pubKey, now.Add(time.Second*60)) { 213 | t.Errorf("revocation should hold, 100 > 50") 214 | } 215 | 216 | encoded, _ := account.Encode(akp) 217 | decoded, _ := DecodeAccountClaims(encoded) 218 | 219 | if !decoded.Exports[0].IsRevokedAt(pubKey, now.Add(time.Second*60)) { 220 | t.Errorf("revocation should last across encoding") 221 | } 222 | 223 | e.ClearRevocation(pubKey) 224 | 225 | if e.IsRevokedAt(pubKey, now) { 226 | t.Errorf("revocations should be cleared") 227 | } 228 | 229 | e.RevokeAt(pubKey, now.Add(time.Second*1000)) 230 | 231 | if !e.IsRevoked(pubKey) { 232 | t.Errorf("revocation be true we revoked in the future") 233 | } 234 | } 235 | 236 | func TestExportTrackLatency(t *testing.T) { 237 | e := &Export{Subject: "foo", Type: Service} 238 | e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} 239 | vr := CreateValidationResults() 240 | e.Validate(vr) 241 | if !vr.IsEmpty() { 242 | t.Errorf("Expected to validate with simple tracking") 243 | } 244 | 245 | e = &Export{Subject: "foo", Type: Stream} 246 | e.Latency = &ServiceLatency{Sampling: 100, Results: "results"} 247 | vr = CreateValidationResults() 248 | e.Validate(vr) 249 | if vr.IsEmpty() { 250 | t.Errorf("adding latency tracking to a stream should have an validation issue") 251 | } 252 | 253 | e = &Export{Subject: "foo", Type: Service} 254 | e.Latency = &ServiceLatency{Sampling: 0, Results: "results"} 255 | vr = CreateValidationResults() 256 | e.Validate(vr) 257 | if vr.IsEmpty() { 258 | t.Errorf("Sampling <1 should have a validation issue") 259 | } 260 | 261 | e = &Export{Subject: "foo", Type: Service} 262 | e.Latency = &ServiceLatency{Sampling: 122, Results: "results"} 263 | vr = CreateValidationResults() 264 | e.Validate(vr) 265 | if vr.IsEmpty() { 266 | t.Errorf("Sampling >100 should have a validation issue") 267 | } 268 | 269 | e = &Export{Subject: "foo", Type: Service} 270 | e.Latency = &ServiceLatency{Sampling: 22, Results: "results.*"} 271 | vr = CreateValidationResults() 272 | e.Validate(vr) 273 | if vr.IsEmpty() { 274 | t.Errorf("Results subject needs to be valid publish subject") 275 | } 276 | } 277 | 278 | func TestExport_Sorting(t *testing.T) { 279 | var exports Exports 280 | exports.Add(&Export{Subject: "x", Type: Service}) 281 | exports.Add(&Export{Subject: "z", Type: Service}) 282 | exports.Add(&Export{Subject: "y", Type: Service}) 283 | if exports[0].Subject != "x" { 284 | t.Fatal("added export not in expected order") 285 | } 286 | sort.Sort(exports) 287 | if exports[0].Subject != "x" && exports[1].Subject != "y" && exports[2].Subject != "z" { 288 | t.Fatal("exports not sorted") 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /v2/v1compat/genericclaims_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestNewGenericClaims(t *testing.T) { 24 | akp := createAccountNKey(t) 25 | apk := publicKey(akp, t) 26 | 27 | uc := NewGenericClaims(apk) 28 | uc.Expires = time.Now().Add(time.Duration(time.Hour)).UTC().Unix() 29 | uc.Name = "alberto" 30 | uc.Audience = "everyone" 31 | uc.NotBefore = time.Now().UTC().Unix() 32 | uc.Tags.Add("one") 33 | uc.Tags.Add("one") 34 | uc.Tags.Add("one") 35 | uc.Tags.Add("TWO") // should become lower case 36 | uc.Tags.Add("three") 37 | 38 | uJwt := encode(uc, akp, t) 39 | 40 | uc2, err := DecodeGeneric(uJwt) 41 | if err != nil { 42 | t.Fatal("failed to decode", err) 43 | } 44 | 45 | AssertEquals(uc.String(), uc2.String(), t) 46 | AssertEquals(uc.Name, uc2.Name, t) 47 | AssertEquals(uc.Audience, uc2.Audience, t) 48 | AssertEquals(uc.Expires, uc2.Expires, t) 49 | AssertEquals(uc.NotBefore, uc2.NotBefore, t) 50 | AssertEquals(uc.Subject, uc2.Subject, t) 51 | 52 | AssertEquals(3, len(uc2.Tags), t) 53 | AssertEquals(true, uc2.Tags.Contains("two"), t) 54 | AssertEquals("one", uc2.Tags[0], t) 55 | AssertEquals("two", uc2.Tags[1], t) 56 | AssertEquals("three", uc2.Tags[2], t) 57 | 58 | AssertEquals(uc.Claims() != nil, true, t) 59 | AssertEquals(uc.Payload() != nil, true, t) 60 | } 61 | -------------------------------------------------------------------------------- /v2/v1compat/genericlaims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import "github.com/nats-io/nkeys" 19 | 20 | // GenericClaims can be used to read a JWT as a map for any non-generic fields 21 | type GenericClaims struct { 22 | ClaimsData 23 | Data map[string]interface{} `json:"nats,omitempty"` 24 | } 25 | 26 | // NewGenericClaims creates a map-based Claims 27 | func NewGenericClaims(subject string) *GenericClaims { 28 | if subject == "" { 29 | return nil 30 | } 31 | c := GenericClaims{} 32 | c.Subject = subject 33 | c.Data = make(map[string]interface{}) 34 | return &c 35 | } 36 | 37 | // DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map 38 | func DecodeGeneric(token string) (*GenericClaims, error) { 39 | v := GenericClaims{} 40 | if err := Decode(token, &v); err != nil { 41 | return nil, err 42 | } 43 | return &v, nil 44 | } 45 | 46 | // Claims returns the standard part of the generic claim 47 | func (gc *GenericClaims) Claims() *ClaimsData { 48 | return &gc.ClaimsData 49 | } 50 | 51 | // Payload returns the custom part of the claims data 52 | func (gc *GenericClaims) Payload() interface{} { 53 | return &gc.Data 54 | } 55 | 56 | // Encode takes a generic claims and creates a JWT string 57 | func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) { 58 | return gc.ClaimsData.Encode(pair, gc) 59 | } 60 | 61 | // Validate checks the generic part of the claims data 62 | func (gc *GenericClaims) Validate(vr *ValidationResults) { 63 | gc.ClaimsData.Validate(vr) 64 | } 65 | 66 | func (gc *GenericClaims) String() string { 67 | return gc.ClaimsData.String(gc) 68 | } 69 | 70 | // ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all 71 | func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte { 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /v2/v1compat/header.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2019 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | // Version is semantic version. 26 | Version = "1.2.2" 27 | 28 | // TokenTypeJwt is the JWT token type supported JWT tokens 29 | // encoded and decoded by this library 30 | TokenTypeJwt = "jwt" 31 | 32 | // AlgorithmNkey is the algorithm supported by JWT tokens 33 | // encoded and decoded by this library 34 | AlgorithmNkey = "ed25519" 35 | ) 36 | 37 | // Header is a JWT Jose Header 38 | type Header struct { 39 | Type string `json:"typ"` 40 | Algorithm string `json:"alg"` 41 | } 42 | 43 | // Parses a header JWT token 44 | func parseHeaders(s string) (*Header, error) { 45 | h, err := decodeString(s) 46 | if err != nil { 47 | return nil, err 48 | } 49 | header := Header{} 50 | if err := json.Unmarshal(h, &header); err != nil { 51 | return nil, err 52 | } 53 | 54 | if err := header.Valid(); err != nil { 55 | return nil, err 56 | } 57 | return &header, nil 58 | } 59 | 60 | // Valid validates the Header. It returns nil if the Header is 61 | // a JWT header, and the algorithm used is the NKEY algorithm. 62 | func (h *Header) Valid() error { 63 | if TokenTypeJwt != strings.ToLower(h.Type) { 64 | return fmt.Errorf("not supported type %q", h.Type) 65 | } 66 | 67 | if alg := strings.ToLower(h.Algorithm); alg != AlgorithmNkey { 68 | if alg == "ed25519-nkey" { 69 | return fmt.Errorf("more recent jwt version") 70 | } 71 | return fmt.Errorf("unexpected %q algorithm", h.Algorithm) 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /v2/v1compat/imports.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2022 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "io" 20 | "net/http" 21 | "net/url" 22 | "time" 23 | ) 24 | 25 | // Import describes a mapping from another account into this one 26 | type Import struct { 27 | Name string `json:"name,omitempty"` 28 | // Subject field in an import is always from the perspective of the 29 | // initial publisher - in the case of a stream it is the account owning 30 | // the stream (the exporter), and in the case of a service it is the 31 | // account making the request (the importer). 32 | Subject Subject `json:"subject,omitempty"` 33 | Account string `json:"account,omitempty"` 34 | Token string `json:"token,omitempty"` 35 | // To field in an import is always from the perspective of the subscriber 36 | // in the case of a stream it is the client of the stream (the importer), 37 | // from the perspective of a service, it is the subscription waiting for 38 | // requests (the exporter). If the field is empty, it will default to the 39 | // value in the Subject field. 40 | To Subject `json:"to,omitempty"` 41 | Type ExportType `json:"type,omitempty"` 42 | } 43 | 44 | // IsService returns true if the import is of type service 45 | func (i *Import) IsService() bool { 46 | return i.Type == Service 47 | } 48 | 49 | // IsStream returns true if the import is of type stream 50 | func (i *Import) IsStream() bool { 51 | return i.Type == Stream 52 | } 53 | 54 | // Validate checks if an import is valid for the wrapping account 55 | func (i *Import) Validate(actPubKey string, vr *ValidationResults) { 56 | if i == nil { 57 | vr.AddError("null import is not allowed") 58 | return 59 | } 60 | if !i.IsService() && !i.IsStream() { 61 | vr.AddError("invalid import type: %q", i.Type) 62 | } 63 | 64 | if i.Account == "" { 65 | vr.AddError("account to import from is not specified") 66 | } 67 | 68 | i.Subject.Validate(vr) 69 | 70 | if i.IsService() && i.Subject.HasWildCards() { 71 | vr.AddError("services cannot have wildcard subject: %q", i.Subject) 72 | } 73 | if i.IsStream() && i.To.HasWildCards() { 74 | vr.AddError("streams cannot have wildcard to subject: %q", i.Subject) 75 | } 76 | 77 | var act *ActivationClaims 78 | 79 | if i.Token != "" { 80 | // Check to see if its an embedded JWT or a URL. 81 | if u, err := url.Parse(i.Token); err == nil && u.Scheme != "" { 82 | c := &http.Client{Timeout: 5 * time.Second} 83 | resp, err := c.Get(u.String()) 84 | if err != nil { 85 | vr.AddError("import %s contains an unreachable token URL %q", i.Subject, i.Token) 86 | } 87 | 88 | if resp != nil { 89 | defer resp.Body.Close() 90 | body, err := io.ReadAll(resp.Body) 91 | if err != nil { 92 | vr.AddError("import %s contains an unreadable token URL %q", i.Subject, i.Token) 93 | } else { 94 | act, err = DecodeActivationClaims(string(body)) 95 | if err != nil { 96 | vr.AddError("import %s contains a URL %q with an invalid activation token", i.Subject, i.Token) 97 | } 98 | } 99 | } 100 | } else { 101 | var err error 102 | act, err = DecodeActivationClaims(i.Token) 103 | if err != nil { 104 | vr.AddError("import %q contains an invalid activation token", i.Subject) 105 | } 106 | } 107 | } 108 | 109 | if act != nil { 110 | if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) { 111 | vr.AddError("activation token doesn't match account for import %q", i.Subject) 112 | } 113 | if act.ClaimsData.Subject != actPubKey { 114 | vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject) 115 | } 116 | if act.ImportType != i.Type { 117 | vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type) 118 | } 119 | act.validateWithTimeChecks(vr, false) 120 | subj := i.Subject 121 | if i.IsService() && i.To != "" { 122 | subj = i.To 123 | } 124 | if !subj.IsContainedIn(act.ImportSubject) { 125 | vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject) 126 | } 127 | } 128 | } 129 | 130 | // Imports is a list of import structs 131 | type Imports []*Import 132 | 133 | // Validate checks if an import is valid for the wrapping account 134 | func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) { 135 | toSet := make(map[Subject]bool, len(*i)) 136 | for _, v := range *i { 137 | if v == nil { 138 | vr.AddError("null import is not allowed") 139 | continue 140 | } 141 | if v.Type == Service { 142 | if _, ok := toSet[v.To]; ok { 143 | vr.AddError("Duplicate To subjects for %q", v.To) 144 | } 145 | toSet[v.To] = true 146 | } 147 | v.Validate(acctPubKey, vr) 148 | } 149 | } 150 | 151 | // Add is a simple way to add imports 152 | func (i *Imports) Add(a ...*Import) { 153 | *i = append(*i, a...) 154 | } 155 | 156 | func (i Imports) Len() int { 157 | return len(i) 158 | } 159 | 160 | func (i Imports) Swap(j, k int) { 161 | i[j], i[k] = i[k], i[j] 162 | } 163 | 164 | func (i Imports) Less(j, k int) bool { 165 | return i[j].Subject < i[k].Subject 166 | } 167 | -------------------------------------------------------------------------------- /v2/v1compat/operator_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "net/url" 22 | "strings" 23 | 24 | "github.com/nats-io/nkeys" 25 | ) 26 | 27 | // Operator specific claims 28 | type Operator struct { 29 | // Slice of real identities (like websites) that can be used to identify the operator. 30 | Identities []Identity `json:"identity,omitempty"` 31 | // Slice of other operator NKeys that can be used to sign on behalf of the main 32 | // operator identity. 33 | SigningKeys StringList `json:"signing_keys,omitempty"` 34 | // AccountServerURL is a partial URL like "https://host.domain.org:/jwt/v1" 35 | // tools will use the prefix and build queries by appending /accounts/ 36 | // or /operator to the path provided. Note this assumes that the account server 37 | // can handle requests in a nats-account-server compatible way. See 38 | // https://github.com/nats-io/nats-account-server. 39 | AccountServerURL string `json:"account_server_url,omitempty"` 40 | // A list of NATS urls (tls://host:port) where tools can connect to the server 41 | // using proper credentials. 42 | OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"` 43 | // Identity of the system account 44 | SystemAccount string `json:"system_account,omitempty"` 45 | } 46 | 47 | // Validate checks the validity of the operators contents 48 | func (o *Operator) Validate(vr *ValidationResults) { 49 | if err := o.validateAccountServerURL(); err != nil { 50 | vr.AddError(err.Error()) 51 | } 52 | 53 | for _, v := range o.validateOperatorServiceURLs() { 54 | if v != nil { 55 | vr.AddError(v.Error()) 56 | } 57 | } 58 | 59 | for _, i := range o.Identities { 60 | i.Validate(vr) 61 | } 62 | 63 | for _, k := range o.SigningKeys { 64 | if !nkeys.IsValidPublicOperatorKey(k) { 65 | vr.AddError("%s is not an operator public key", k) 66 | } 67 | } 68 | if o.SystemAccount != "" { 69 | if !nkeys.IsValidPublicAccountKey(o.SystemAccount) { 70 | vr.AddError("%s is not an account public key", o.SystemAccount) 71 | } 72 | } 73 | } 74 | 75 | func (o *Operator) validateAccountServerURL() error { 76 | if o.AccountServerURL != "" { 77 | // We don't care what kind of URL it is so long as it parses 78 | // and has a protocol. The account server may impose additional 79 | // constraints on the type of URLs that it is able to notify to 80 | u, err := url.Parse(o.AccountServerURL) 81 | if err != nil { 82 | return fmt.Errorf("error parsing account server url: %v", err) 83 | } 84 | if u.Scheme == "" { 85 | return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL) 86 | } 87 | } 88 | return nil 89 | } 90 | 91 | // ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url. 92 | func ValidateOperatorServiceURL(v string) error { 93 | // should be possible for the service url to not be expressed 94 | if v == "" { 95 | return nil 96 | } 97 | u, err := url.Parse(v) 98 | if err != nil { 99 | return fmt.Errorf("error parsing operator service url %q: %v", v, err) 100 | } 101 | 102 | if u.User != nil { 103 | return fmt.Errorf("operator service url %q - credentials are not supported", v) 104 | } 105 | 106 | if u.Path != "" { 107 | return fmt.Errorf("operator service url %q - paths are not supported", v) 108 | } 109 | 110 | lcs := strings.ToLower(u.Scheme) 111 | switch lcs { 112 | case "nats": 113 | return nil 114 | case "tls": 115 | return nil 116 | default: 117 | return fmt.Errorf("operator service url %q - protocol not supported (only 'nats' or 'tls' only)", v) 118 | } 119 | } 120 | 121 | func (o *Operator) validateOperatorServiceURLs() []error { 122 | var errs []error 123 | for _, v := range o.OperatorServiceURLs { 124 | if v != "" { 125 | if err := ValidateOperatorServiceURL(v); err != nil { 126 | errs = append(errs, err) 127 | } 128 | } 129 | } 130 | return errs 131 | } 132 | 133 | // OperatorClaims define the data for an operator JWT 134 | type OperatorClaims struct { 135 | ClaimsData 136 | Operator `json:"nats,omitempty"` 137 | } 138 | 139 | // NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key 140 | func NewOperatorClaims(subject string) *OperatorClaims { 141 | if subject == "" { 142 | return nil 143 | } 144 | c := &OperatorClaims{} 145 | c.Subject = subject 146 | return c 147 | } 148 | 149 | // DidSign checks the claims against the operator's public key and its signing keys 150 | func (oc *OperatorClaims) DidSign(op Claims) bool { 151 | if op == nil { 152 | return false 153 | } 154 | issuer := op.Claims().Issuer 155 | if issuer == oc.Subject { 156 | return true 157 | } 158 | return oc.SigningKeys.Contains(issuer) 159 | } 160 | 161 | // Deprecated: AddSigningKey, use claim.SigningKeys.Add() 162 | func (oc *OperatorClaims) AddSigningKey(pk string) { 163 | oc.SigningKeys.Add(pk) 164 | } 165 | 166 | // Encode the claims into a JWT string 167 | func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { 168 | if !nkeys.IsValidPublicOperatorKey(oc.Subject) { 169 | return "", errors.New("expected subject to be an operator public key") 170 | } 171 | err := oc.validateAccountServerURL() 172 | if err != nil { 173 | return "", err 174 | } 175 | oc.ClaimsData.Type = OperatorClaim 176 | return oc.ClaimsData.Encode(pair, oc) 177 | } 178 | 179 | // DecodeOperatorClaims tries to create an operator claims from a JWt string 180 | func DecodeOperatorClaims(token string) (*OperatorClaims, error) { 181 | v := OperatorClaims{} 182 | if err := Decode(token, &v); err != nil { 183 | return nil, err 184 | } 185 | return &v, nil 186 | } 187 | 188 | func (oc *OperatorClaims) String() string { 189 | return oc.ClaimsData.String(oc) 190 | } 191 | 192 | // Payload returns the operator specific data for an operator JWT 193 | func (oc *OperatorClaims) Payload() interface{} { 194 | return &oc.Operator 195 | } 196 | 197 | // Validate the contents of the claims 198 | func (oc *OperatorClaims) Validate(vr *ValidationResults) { 199 | oc.ClaimsData.Validate(vr) 200 | oc.Operator.Validate(vr) 201 | } 202 | 203 | // ExpectedPrefixes defines the nkey types that can sign operator claims, operator 204 | func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte { 205 | return []nkeys.PrefixByte{nkeys.PrefixByteOperator} 206 | } 207 | 208 | // Claims returns the generic claims data 209 | func (oc *OperatorClaims) Claims() *ClaimsData { 210 | return &oc.ClaimsData 211 | } 212 | -------------------------------------------------------------------------------- /v2/v1compat/revocation_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "time" 20 | ) 21 | 22 | const All = "*" 23 | 24 | // RevocationList is used to store a mapping of public keys to unix timestamps 25 | type RevocationList map[string]int64 26 | 27 | // Revoke enters a revocation by publickey and timestamp into this export 28 | // If there is already a revocation for this public key that is newer, it is kept. 29 | func (r RevocationList) Revoke(pubKey string, timestamp time.Time) { 30 | newTS := timestamp.Unix() 31 | if ts, ok := r[pubKey]; ok && ts > newTS { 32 | return 33 | } 34 | 35 | r[pubKey] = newTS 36 | } 37 | 38 | // ClearRevocation removes any revocation for the public key 39 | func (r RevocationList) ClearRevocation(pubKey string) { 40 | delete(r, pubKey) 41 | } 42 | 43 | // IsRevoked checks if the public key is in the revoked list with a timestamp later than 44 | // the one passed in. Generally this method is called with an issue time but other time's can 45 | // be used for testing. 46 | func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool { 47 | if r.allRevoked(timestamp) { 48 | return true 49 | } 50 | ts, ok := r[pubKey] 51 | return ok && ts >= timestamp.Unix() 52 | } 53 | 54 | // allRevoked returns true if All is set and the timestamp is later or same as the 55 | // one passed. This is called by IsRevoked. 56 | func (r RevocationList) allRevoked(timestamp time.Time) bool { 57 | ts, ok := r[All] 58 | return ok && ts >= timestamp.Unix() 59 | } 60 | -------------------------------------------------------------------------------- /v2/v1compat/server_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2020 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | 21 | "github.com/nats-io/nkeys" 22 | ) 23 | 24 | // Deprecated: ServerClaims are not supported 25 | type Server struct { 26 | Permissions 27 | Cluster string `json:"cluster,omitempty"` 28 | } 29 | 30 | // Validate checks the cluster and permissions for a server JWT 31 | func (s *Server) Validate(vr *ValidationResults) { 32 | if s.Cluster == "" { 33 | vr.AddError("servers can't contain an empty cluster") 34 | } 35 | } 36 | 37 | // Deprecated: ServerClaims are not supported 38 | type ServerClaims struct { 39 | ClaimsData 40 | Server `json:"nats,omitempty"` 41 | } 42 | 43 | // Deprecated: ServerClaims are not supported 44 | func NewServerClaims(subject string) *ServerClaims { 45 | if subject == "" { 46 | return nil 47 | } 48 | c := &ServerClaims{} 49 | c.Subject = subject 50 | return c 51 | } 52 | 53 | // Encode tries to turn the server claims into a JWT string 54 | func (s *ServerClaims) Encode(pair nkeys.KeyPair) (string, error) { 55 | if !nkeys.IsValidPublicServerKey(s.Subject) { 56 | return "", errors.New("expected subject to be a server public key") 57 | } 58 | s.ClaimsData.Type = ServerClaim 59 | return s.ClaimsData.Encode(pair, s) 60 | } 61 | 62 | // Deprecated: ServerClaims are not supported 63 | func DecodeServerClaims(token string) (*ServerClaims, error) { 64 | v := ServerClaims{} 65 | if err := Decode(token, &v); err != nil { 66 | return nil, err 67 | } 68 | return &v, nil 69 | } 70 | 71 | func (s *ServerClaims) String() string { 72 | return s.ClaimsData.String(s) 73 | } 74 | 75 | // Payload returns the server specific data 76 | func (s *ServerClaims) Payload() interface{} { 77 | return &s.Server 78 | } 79 | 80 | // Validate checks the generic and server data in the server claims 81 | func (s *ServerClaims) Validate(vr *ValidationResults) { 82 | s.ClaimsData.Validate(vr) 83 | s.Server.Validate(vr) 84 | } 85 | 86 | // ExpectedPrefixes defines the types that can encode a server JWT, operator or cluster 87 | func (s *ServerClaims) ExpectedPrefixes() []nkeys.PrefixByte { 88 | return []nkeys.PrefixByte{nkeys.PrefixByteOperator, nkeys.PrefixByteCluster} 89 | } 90 | 91 | // Claims returns the generic data 92 | func (s *ServerClaims) Claims() *ClaimsData { 93 | return &s.ClaimsData 94 | } 95 | -------------------------------------------------------------------------------- /v2/v1compat/server_claims_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "testing" 20 | "time" 21 | 22 | "github.com/nats-io/nkeys" 23 | ) 24 | 25 | func TestNewServerClaims(t *testing.T) { 26 | ckp := createClusterNKey(t) 27 | skp := createServerNKey(t) 28 | 29 | uc := NewServerClaims(publicKey(skp, t)) 30 | uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() 31 | uJwt := encode(uc, ckp, t) 32 | 33 | uc2, err := DecodeServerClaims(uJwt) 34 | if err != nil { 35 | t.Fatal("failed to decode", err) 36 | } 37 | 38 | AssertEquals(uc.String(), uc2.String(), t) 39 | 40 | AssertEquals(uc.Claims() != nil, true, t) 41 | AssertEquals(uc.Payload() != nil, true, t) 42 | } 43 | 44 | func TestServerClaimsIssuer(t *testing.T) { 45 | ckp := createClusterNKey(t) 46 | skp := createServerNKey(t) 47 | 48 | uc := NewServerClaims(publicKey(skp, t)) 49 | uc.Expires = time.Now().Add(time.Duration(time.Hour)).Unix() 50 | uJwt := encode(uc, ckp, t) 51 | 52 | temp, err := DecodeGeneric(uJwt) 53 | if err != nil { 54 | t.Fatal("failed to decode", err) 55 | } 56 | 57 | type kpInputs struct { 58 | name string 59 | kp nkeys.KeyPair 60 | ok bool 61 | } 62 | 63 | inputs := []kpInputs{ 64 | {"account", createAccountNKey(t), false}, 65 | {"user", createUserNKey(t), false}, 66 | {"operator", createOperatorNKey(t), true}, 67 | {"server", createServerNKey(t), false}, 68 | {"cluster", createClusterNKey(t), true}, 69 | } 70 | 71 | for _, i := range inputs { 72 | bad := encode(temp, i.kp, t) 73 | _, err = DecodeServerClaims(bad) 74 | if i.ok && err != nil { 75 | t.Fatalf("unexpected error for %q: %v", i.name, err) 76 | } 77 | if !i.ok && err == nil { 78 | t.Logf("should have failed to decode server signed by %q", i.name) 79 | t.Fail() 80 | } 81 | } 82 | } 83 | 84 | func TestServerSubjects(t *testing.T) { 85 | type kpInputs struct { 86 | name string 87 | kp nkeys.KeyPair 88 | ok bool 89 | } 90 | 91 | inputs := []kpInputs{ 92 | {"account", createAccountNKey(t), false}, 93 | {"cluster", createClusterNKey(t), false}, 94 | {"operator", createOperatorNKey(t), false}, 95 | {"server", createServerNKey(t), true}, 96 | {"user", createUserNKey(t), false}, 97 | } 98 | 99 | for _, i := range inputs { 100 | c := NewServerClaims(publicKey(i.kp, t)) 101 | _, err := c.Encode(createOperatorNKey(t)) 102 | if i.ok && err != nil { 103 | t.Fatalf("unexpected error for %q: %v", i.name, err) 104 | } 105 | if !i.ok && err == nil { 106 | t.Logf("should have failed to encode server with with %q subject", i.name) 107 | t.Fail() 108 | } 109 | } 110 | } 111 | 112 | func TestNewNilServerClaims(t *testing.T) { 113 | v := NewServerClaims("") 114 | if v != nil { 115 | t.Fatal("expected nil user claim") 116 | } 117 | } 118 | 119 | func TestServerType(t *testing.T) { 120 | c := NewServerClaims(publicKey(createServerNKey(t), t)) 121 | s := encode(c, createClusterNKey(t), t) 122 | u, err := DecodeServerClaims(s) 123 | if err != nil { 124 | t.Fatalf("failed to decode server claim: %v", err) 125 | } 126 | 127 | if ServerClaim != u.Type { 128 | t.Fatalf("type is unexpected %q (wanted server)", u.Type) 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /v2/v1compat/types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "os" 20 | "regexp" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestVersion(t *testing.T) { 26 | // Semantic versioning 27 | verRe := regexp.MustCompile(`\d+.\d+.\d+(-\S+)?`) 28 | if !verRe.MatchString(Version) { 29 | t.Fatalf("Version not compatible with semantic versioning: %q", Version) 30 | } 31 | } 32 | 33 | func TestVersionMatchesTag(t *testing.T) { 34 | tag := os.Getenv("TRAVIS_TAG") 35 | if tag == "" { 36 | t.SkipNow() 37 | } 38 | // We expect a tag of the form vX.Y.Z. If that's not the case, 39 | // we need someone to have a look. So fail if first letter is not 40 | // a `v` 41 | if len(tag) < 2 || tag[0] != 'v' { 42 | t.Fatalf("Expect tag to start with `v`, tag is: %s", tag) 43 | } 44 | // Look only at tag from current 'v', that is v1 for this file. 45 | if tag[1] != '1' { 46 | // Ignore, it is not a v1 tag. 47 | return 48 | } 49 | // Strip the `v` from the tag for the version comparison. 50 | if Version != tag[1:] { 51 | t.Fatalf("Version (%s) does not match tag (%s)", Version, tag[1:]) 52 | } 53 | } 54 | 55 | func TestTimeRangeValidation(t *testing.T) { 56 | tr := TimeRange{ 57 | Start: "hello", 58 | End: "03:15:00", 59 | } 60 | 61 | vr := CreateValidationResults() 62 | tr.Validate(vr) 63 | 64 | if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { 65 | t.Error("bad start should be invalid") 66 | } 67 | 68 | if !strings.Contains(vr.Issues[0].Error(), tr.Start) { 69 | t.Error("error should contain the faulty value") 70 | } 71 | 72 | tr = TimeRange{ 73 | Start: "15:43:22", 74 | End: "27:11:11", 75 | } 76 | 77 | vr = CreateValidationResults() 78 | tr.Validate(vr) 79 | 80 | if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { 81 | t.Error("bad end should be invalid") 82 | } 83 | 84 | if !strings.Contains(vr.Issues[0].Error(), tr.End) { 85 | t.Error("error should contain the faulty value") 86 | } 87 | 88 | tr = TimeRange{ 89 | Start: "", 90 | End: "03:15:00", 91 | } 92 | 93 | vr = CreateValidationResults() 94 | tr.Validate(vr) 95 | 96 | if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { 97 | t.Error("bad start should be invalid") 98 | } 99 | 100 | tr = TimeRange{ 101 | Start: "15:43:22", 102 | End: "", 103 | } 104 | 105 | vr = CreateValidationResults() 106 | tr.Validate(vr) 107 | 108 | if vr.IsEmpty() || len(vr.Issues) != 1 || !vr.IsBlocking(true) { 109 | t.Error("bad end should be invalid") 110 | } 111 | } 112 | 113 | func TestTagList(t *testing.T) { 114 | tags := TagList{} 115 | 116 | tags.Add("one") 117 | 118 | AssertEquals(true, tags.Contains("one"), t) 119 | AssertEquals(true, tags.Contains("ONE"), t) 120 | AssertEquals("one", tags[0], t) 121 | 122 | tags.Add("TWO") 123 | 124 | AssertEquals(true, tags.Contains("two"), t) 125 | AssertEquals(true, tags.Contains("TWO"), t) 126 | AssertEquals("two", tags[1], t) 127 | 128 | tags.Remove("ONE") 129 | AssertEquals("two", tags[0], t) 130 | AssertEquals(false, tags.Contains("one"), t) 131 | AssertEquals(false, tags.Contains("ONE"), t) 132 | } 133 | 134 | func TestStringList(t *testing.T) { 135 | slist := StringList{} 136 | 137 | slist.Add("one") 138 | 139 | AssertEquals(true, slist.Contains("one"), t) 140 | AssertEquals(false, slist.Contains("ONE"), t) 141 | AssertEquals("one", slist[0], t) 142 | 143 | slist.Add("TWO") 144 | 145 | AssertEquals(false, slist.Contains("two"), t) 146 | AssertEquals(true, slist.Contains("TWO"), t) 147 | AssertEquals("TWO", slist[1], t) 148 | 149 | slist.Remove("ONE") 150 | AssertEquals("one", slist[0], t) 151 | AssertEquals(true, slist.Contains("one"), t) 152 | AssertEquals(false, slist.Contains("ONE"), t) 153 | 154 | slist.Add("ONE") 155 | AssertEquals(true, slist.Contains("one"), t) 156 | AssertEquals(true, slist.Contains("ONE"), t) 157 | AssertEquals(3, len(slist), t) 158 | 159 | slist.Remove("one") 160 | AssertEquals("TWO", slist[0], t) 161 | AssertEquals(false, slist.Contains("one"), t) 162 | AssertEquals(true, slist.Contains("ONE"), t) 163 | } 164 | 165 | func TestSubjectValid(t *testing.T) { 166 | var s Subject 167 | 168 | vr := CreateValidationResults() 169 | s.Validate(vr) 170 | if !vr.IsBlocking(false) { 171 | t.Fatalf("Empty string is not a valid subjects") 172 | } 173 | 174 | s = "has spaces" 175 | vr = CreateValidationResults() 176 | s.Validate(vr) 177 | if !vr.IsBlocking(false) { 178 | t.Fatalf("Subjects cannot contain spaces") 179 | } 180 | 181 | s = "has.spa ces.and.tokens" 182 | vr = CreateValidationResults() 183 | s.Validate(vr) 184 | if !vr.IsBlocking(false) { 185 | t.Fatalf("Subjects cannot have spaces") 186 | } 187 | 188 | s = "one" 189 | vr = CreateValidationResults() 190 | s.Validate(vr) 191 | if !vr.IsEmpty() { 192 | t.Fatalf("%s is a valid subject", s) 193 | } 194 | 195 | s = "one.two.three" 196 | vr = CreateValidationResults() 197 | s.Validate(vr) 198 | if !vr.IsEmpty() { 199 | t.Fatalf("%s is a valid subject", s) 200 | } 201 | } 202 | 203 | func TestSubjectHasWildCards(t *testing.T) { 204 | s := Subject("one") 205 | AssertEquals(false, s.HasWildCards(), t) 206 | 207 | s = "one.two.three" 208 | AssertEquals(false, s.HasWildCards(), t) 209 | 210 | s = "*" 211 | AssertEquals(true, s.HasWildCards(), t) 212 | 213 | s = "one.*.three" 214 | AssertEquals(true, s.HasWildCards(), t) 215 | 216 | s = "*.two.three" 217 | AssertEquals(true, s.HasWildCards(), t) 218 | 219 | s = "one.two.*" 220 | AssertEquals(true, s.HasWildCards(), t) 221 | 222 | s = "one.>" 223 | AssertEquals(true, s.HasWildCards(), t) 224 | 225 | s = "one.two.>" 226 | AssertEquals(true, s.HasWildCards(), t) 227 | 228 | s = ">" 229 | AssertEquals(true, s.HasWildCards(), t) 230 | } 231 | 232 | func TestSubjectContainment(t *testing.T) { 233 | var s Subject 234 | var o Subject 235 | 236 | s = "one.two.three" 237 | o = "one.two.three" 238 | AssertEquals(true, s.IsContainedIn(o), t) 239 | 240 | s = "one.two.three" 241 | o = "one.two.*" 242 | AssertEquals(true, s.IsContainedIn(o), t) 243 | 244 | s = "one.two.three" 245 | o = "one.*.three" 246 | AssertEquals(true, s.IsContainedIn(o), t) 247 | 248 | s = "one.two.three" 249 | o = "*.two.three" 250 | AssertEquals(true, s.IsContainedIn(o), t) 251 | 252 | s = "one.two.three" 253 | o = "one.two.>" 254 | AssertEquals(true, s.IsContainedIn(o), t) 255 | 256 | s = "one.two.three" 257 | o = "one.>" 258 | AssertEquals(true, s.IsContainedIn(o), t) 259 | 260 | s = "one.two.three" 261 | o = ">" 262 | AssertEquals(true, s.IsContainedIn(o), t) 263 | 264 | s = "one.two.three" 265 | o = "one.two" 266 | AssertEquals(false, s.IsContainedIn(o), t) 267 | 268 | s = "one" 269 | o = "one.two" 270 | AssertEquals(false, s.IsContainedIn(o), t) 271 | } 272 | -------------------------------------------------------------------------------- /v2/v1compat/user_claims.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2019 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | 21 | "github.com/nats-io/nkeys" 22 | ) 23 | 24 | // User defines the user specific data in a user JWT 25 | type User struct { 26 | Permissions 27 | Limits 28 | BearerToken bool `json:"bearer_token,omitempty"` 29 | } 30 | 31 | // Validate checks the permissions and limits in a User jwt 32 | func (u *User) Validate(vr *ValidationResults) { 33 | u.Permissions.Validate(vr) 34 | u.Limits.Validate(vr) 35 | // When BearerToken is true server will ignore any nonce-signing verification 36 | } 37 | 38 | // UserClaims defines a user JWT 39 | type UserClaims struct { 40 | ClaimsData 41 | User `json:"nats,omitempty"` 42 | // IssuerAccount stores the public key for the account the issuer represents. 43 | // When set, the claim was issued by a signing key. 44 | IssuerAccount string `json:"issuer_account,omitempty"` 45 | } 46 | 47 | // NewUserClaims creates a user JWT with the specific subject/public key 48 | func NewUserClaims(subject string) *UserClaims { 49 | if subject == "" { 50 | return nil 51 | } 52 | c := &UserClaims{} 53 | c.Subject = subject 54 | return c 55 | } 56 | 57 | // Encode tries to turn the user claims into a JWT string 58 | func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) { 59 | if !nkeys.IsValidPublicUserKey(u.Subject) { 60 | return "", errors.New("expected subject to be user public key") 61 | } 62 | u.ClaimsData.Type = UserClaim 63 | return u.ClaimsData.Encode(pair, u) 64 | } 65 | 66 | // DecodeUserClaims tries to parse a user claims from a JWT string 67 | func DecodeUserClaims(token string) (*UserClaims, error) { 68 | v := UserClaims{} 69 | if err := Decode(token, &v); err != nil { 70 | return nil, err 71 | } 72 | return &v, nil 73 | } 74 | 75 | // Validate checks the generic and specific parts of the user jwt 76 | func (u *UserClaims) Validate(vr *ValidationResults) { 77 | u.ClaimsData.Validate(vr) 78 | u.User.Validate(vr) 79 | if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) { 80 | vr.AddError("account_id is not an account public key") 81 | } 82 | } 83 | 84 | // ExpectedPrefixes defines the types that can encode a user JWT, account 85 | func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte { 86 | return []nkeys.PrefixByte{nkeys.PrefixByteAccount} 87 | } 88 | 89 | // Claims returns the generic data from a user jwt 90 | func (u *UserClaims) Claims() *ClaimsData { 91 | return &u.ClaimsData 92 | } 93 | 94 | // Payload returns the user specific data from a user JWT 95 | func (u *UserClaims) Payload() interface{} { 96 | return &u.User 97 | } 98 | 99 | func (u *UserClaims) String() string { 100 | return u.ClaimsData.String(u) 101 | } 102 | 103 | // IsBearerToken returns true if nonce-signing requirements should be skipped 104 | func (u *UserClaims) IsBearerToken() bool { 105 | return u.BearerToken 106 | } 107 | -------------------------------------------------------------------------------- /v2/v1compat/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "runtime" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/nats-io/nkeys" 26 | ) 27 | 28 | func Trace(message string) string { 29 | lines := make([]string, 0, 32) 30 | err := errors.New(message) 31 | msg := err.Error() 32 | lines = append(lines, msg) 33 | 34 | for i := 2; true; i++ { 35 | _, file, line, ok := runtime.Caller(i) 36 | if !ok { 37 | break 38 | } 39 | msg := fmt.Sprintf("%s:%d", file, line) 40 | lines = append(lines, msg) 41 | } 42 | return strings.Join(lines, "\n") 43 | } 44 | 45 | func AssertEquals(expected, v interface{}, t *testing.T) { 46 | if expected != v { 47 | t.Fatalf("%v", Trace(fmt.Sprintf("The expected value %v != %v", expected, v))) 48 | } 49 | } 50 | 51 | func createAccountNKey(t *testing.T) nkeys.KeyPair { 52 | kp, err := nkeys.CreateAccount() 53 | if err != nil { 54 | t.Fatal("error creating account kp", err) 55 | } 56 | return kp 57 | } 58 | 59 | func createUserNKey(t *testing.T) nkeys.KeyPair { 60 | kp, err := nkeys.CreateUser() 61 | if err != nil { 62 | t.Fatal("error creating account kp", err) 63 | } 64 | return kp 65 | } 66 | 67 | func createOperatorNKey(t *testing.T) nkeys.KeyPair { 68 | kp, err := nkeys.CreateOperator() 69 | if err != nil { 70 | t.Fatal("error creating operator kp", err) 71 | } 72 | return kp 73 | } 74 | 75 | func createServerNKey(t *testing.T) nkeys.KeyPair { 76 | kp, err := nkeys.CreateServer() 77 | if err != nil { 78 | t.Fatal("error creating server kp", err) 79 | } 80 | return kp 81 | } 82 | 83 | func createClusterNKey(t *testing.T) nkeys.KeyPair { 84 | kp, err := nkeys.CreateCluster() 85 | if err != nil { 86 | t.Fatal("error creating cluster kp", err) 87 | } 88 | return kp 89 | } 90 | 91 | func publicKey(kp nkeys.KeyPair, t *testing.T) string { 92 | pk, err := kp.PublicKey() 93 | if err != nil { 94 | t.Fatal("error reading public key", err) 95 | } 96 | return string(pk) 97 | } 98 | 99 | func seedKey(kp nkeys.KeyPair, t *testing.T) []byte { 100 | sk, err := kp.Seed() 101 | if err != nil { 102 | t.Fatal("error reading seed", err) 103 | } 104 | return sk 105 | } 106 | 107 | func encode(c Claims, kp nkeys.KeyPair, t *testing.T) string { 108 | s, err := c.Encode(kp) 109 | if err != nil { 110 | t.Fatal("error encoding claim", err) 111 | } 112 | return s 113 | } 114 | -------------------------------------------------------------------------------- /v2/v1compat/validation.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | ) 22 | 23 | // ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error 24 | type ValidationIssue struct { 25 | Description string 26 | Blocking bool 27 | TimeCheck bool 28 | } 29 | 30 | func (ve *ValidationIssue) Error() string { 31 | return ve.Description 32 | } 33 | 34 | // ValidationResults is a list of ValidationIssue pointers 35 | type ValidationResults struct { 36 | Issues []*ValidationIssue 37 | } 38 | 39 | // CreateValidationResults creates an empty list of validation issues 40 | func CreateValidationResults() *ValidationResults { 41 | issues := []*ValidationIssue{} 42 | return &ValidationResults{ 43 | Issues: issues, 44 | } 45 | } 46 | 47 | // Add appends an issue to the list 48 | func (v *ValidationResults) Add(vi *ValidationIssue) { 49 | v.Issues = append(v.Issues, vi) 50 | } 51 | 52 | // AddError creates a new validation error and adds it to the list 53 | func (v *ValidationResults) AddError(format string, args ...interface{}) { 54 | v.Add(&ValidationIssue{ 55 | Description: fmt.Sprintf(format, args...), 56 | Blocking: true, 57 | TimeCheck: false, 58 | }) 59 | } 60 | 61 | // AddTimeCheck creates a new validation issue related to a time check and adds it to the list 62 | func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) { 63 | v.Add(&ValidationIssue{ 64 | Description: fmt.Sprintf(format, args...), 65 | Blocking: false, 66 | TimeCheck: true, 67 | }) 68 | } 69 | 70 | // AddWarning creates a new validation warning and adds it to the list 71 | func (v *ValidationResults) AddWarning(format string, args ...interface{}) { 72 | v.Add(&ValidationIssue{ 73 | Description: fmt.Sprintf(format, args...), 74 | Blocking: false, 75 | TimeCheck: false, 76 | }) 77 | } 78 | 79 | // IsBlocking returns true if the list contains a blocking error 80 | func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool { 81 | for _, i := range v.Issues { 82 | if i.Blocking { 83 | return true 84 | } 85 | 86 | if includeTimeChecks && i.TimeCheck { 87 | return true 88 | } 89 | } 90 | return false 91 | } 92 | 93 | // IsEmpty returns true if the list is empty 94 | func (v *ValidationResults) IsEmpty() bool { 95 | return len(v.Issues) == 0 96 | } 97 | 98 | // Errors returns only blocking issues as errors 99 | func (v *ValidationResults) Errors() []error { 100 | var errs []error 101 | for _, v := range v.Issues { 102 | if v.Blocking { 103 | errs = append(errs, errors.New(v.Description)) 104 | } 105 | } 106 | return errs 107 | } 108 | 109 | // Warnings returns only non blocking issues as strings 110 | func (v *ValidationResults) Warnings() []string { 111 | var errs []string 112 | for _, v := range v.Issues { 113 | if !v.Blocking { 114 | errs = append(errs, v.Description) 115 | } 116 | } 117 | return errs 118 | } 119 | -------------------------------------------------------------------------------- /v2/validation.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 The NATS Authors 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package jwt 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | ) 22 | 23 | // ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error 24 | type ValidationIssue struct { 25 | Description string 26 | Blocking bool 27 | TimeCheck bool 28 | } 29 | 30 | func (ve *ValidationIssue) Error() string { 31 | return ve.Description 32 | } 33 | 34 | // ValidationResults is a list of ValidationIssue pointers 35 | type ValidationResults struct { 36 | Issues []*ValidationIssue 37 | } 38 | 39 | // CreateValidationResults creates an empty list of validation issues 40 | func CreateValidationResults() *ValidationResults { 41 | var issues []*ValidationIssue 42 | return &ValidationResults{ 43 | Issues: issues, 44 | } 45 | } 46 | 47 | // Add appends an issue to the list 48 | func (v *ValidationResults) Add(vi *ValidationIssue) { 49 | v.Issues = append(v.Issues, vi) 50 | } 51 | 52 | // AddError creates a new validation error and adds it to the list 53 | func (v *ValidationResults) AddError(format string, args ...interface{}) { 54 | v.Add(&ValidationIssue{ 55 | Description: fmt.Sprintf(format, args...), 56 | Blocking: true, 57 | TimeCheck: false, 58 | }) 59 | } 60 | 61 | // AddTimeCheck creates a new validation issue related to a time check and adds it to the list 62 | func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) { 63 | v.Add(&ValidationIssue{ 64 | Description: fmt.Sprintf(format, args...), 65 | Blocking: false, 66 | TimeCheck: true, 67 | }) 68 | } 69 | 70 | // AddWarning creates a new validation warning and adds it to the list 71 | func (v *ValidationResults) AddWarning(format string, args ...interface{}) { 72 | v.Add(&ValidationIssue{ 73 | Description: fmt.Sprintf(format, args...), 74 | Blocking: false, 75 | TimeCheck: false, 76 | }) 77 | } 78 | 79 | // IsBlocking returns true if the list contains a blocking error 80 | func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool { 81 | for _, i := range v.Issues { 82 | if i.Blocking { 83 | return true 84 | } 85 | 86 | if includeTimeChecks && i.TimeCheck { 87 | return true 88 | } 89 | } 90 | return false 91 | } 92 | 93 | // IsEmpty returns true if the list is empty 94 | func (v *ValidationResults) IsEmpty() bool { 95 | return len(v.Issues) == 0 96 | } 97 | 98 | // Errors returns only blocking issues as errors 99 | func (v *ValidationResults) Errors() []error { 100 | var errs []error 101 | for _, v := range v.Issues { 102 | if v.Blocking { 103 | errs = append(errs, errors.New(v.Description)) 104 | } 105 | } 106 | return errs 107 | } 108 | 109 | // Warnings returns only non blocking issues as strings 110 | func (v *ValidationResults) Warnings() []string { 111 | var errs []string 112 | for _, v := range v.Issues { 113 | if !v.Blocking { 114 | errs = append(errs, v.Description) 115 | } 116 | } 117 | return errs 118 | } 119 | --------------------------------------------------------------------------------