diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 72391d2..1210762 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -7,10 +7,10 @@ jobs: strategy: matrix: include: - - go: "stable" + - go: stable os: ubuntu-latest canonical: true - - go: "stable" + - go: stable os: windows-latest canonical: false @@ -25,7 +25,7 @@ jobs: fetch-depth: 1 - name: Setup Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{matrix.go}} diff --git a/v2/account_claims.go b/v2/account_claims.go index 05850fc..9da374a 100644 --- a/v2/account_claims.go +++ b/v2/account_claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -133,7 +133,7 @@ func (o *OperatorLimits) Validate(vr *ValidationResults) { } } -// Mapping for publishes +// WeightedMapping for publishes type WeightedMapping struct { Subject Subject `json:"subject"` Weight uint8 `json:"weight,omitempty"` @@ -177,13 +177,13 @@ func (a *Account) AddMapping(sub Subject, to ...WeightedMapping) { a.Mappings[sub] = to } -// Enable external authorization for account users. +// ExternalAuthorization enables external authorization for account users. // AuthUsers are those users specified to bypass the authorization callout and should be used for the authorization service itself. // AllowedAccounts specifies which accounts, if any, that the authorization service can bind an authorized user to. // The authorization response, a user JWT, will still need to be signed by the correct account. // If optional XKey is specified, that is the public xkey (x25519) and the server will encrypt the request such that only the // holder of the private key can decrypt. The auth service can also optionally encrypt the response back to the server using it's -// publick xkey which will be in the authorization request. +// public xkey which will be in the authorization request. type ExternalAuthorization struct { AuthUsers StringList `json:"auth_users,omitempty"` AllowedAccounts StringList `json:"allowed_accounts,omitempty"` @@ -194,12 +194,12 @@ func (ac *ExternalAuthorization) IsEnabled() bool { return len(ac.AuthUsers) > 0 } -// Helper function to determine if external authorization is enabled. +// HasExternalAuthorization helper function to determine if external authorization is enabled. func (a *Account) HasExternalAuthorization() bool { return a.Authorization.IsEnabled() } -// Helper function to setup external authorization. +// EnableExternalAuthorization helper function to setup external authorization. func (a *Account) EnableExternalAuthorization(users ...string) { a.Authorization.AuthUsers.Add(users...) } @@ -357,13 +357,17 @@ func NewAccountClaims(subject string) *AccountClaims { // Encode converts account claims into a JWT string func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) { + return a.EncodeWithSigner(pair, nil) +} + +func (a *AccountClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicAccountKey(a.Subject) { return "", errors.New("expected subject to be account public key") } sort.Sort(a.Exports) sort.Sort(a.Imports) a.Type = AccountClaim - return a.ClaimsData.encode(pair, a) + return a.ClaimsData.encode(pair, a, fn) } // DecodeAccountClaims decodes account claims from a JWT string diff --git a/v2/account_claims_test.go b/v2/account_claims_test.go index 5930313..0869ee1 100644 --- a/v2/account_claims_test.go +++ b/v2/account_claims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -1018,3 +1018,43 @@ func TestClusterTraffic_Valid(t *testing.T) { } } } + +func TestSignFn(t *testing.T) { + okp := createOperatorNKey(t) + opub := publicKey(okp, t) + opk, err := nkeys.FromPublicKey(opub) + if err != nil { + t.Fatal(err) + } + + akp := createAccountNKey(t) + pub := publicKey(akp, t) + + var ok bool + ac := NewAccountClaims(pub) + ac.Name = "A" + s, err := ac.EncodeWithSigner(opk, func(pub string, data []byte) ([]byte, error) { + if pub != opub { + t.Fatal("expected pub key in callback to match") + } + ok = true + return okp.Sign(data) + }) + + if err != nil { + t.Fatal("error encoding") + } + if !ok { + t.Fatal("expected ok to be true") + } + + ac, err = DecodeAccountClaims(s) + if err != nil { + t.Fatal("error decoding encoded jwt") + } + vr := CreateValidationResults() + ac.Validate(vr) + if !vr.IsEmpty() { + t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) + } +} diff --git a/v2/activation_claims.go b/v2/activation_claims.go index 827658e..63fe788 100644 --- a/v2/activation_claims.go +++ b/v2/activation_claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -72,11 +72,15 @@ func NewActivationClaims(subject string) *ActivationClaims { // Encode turns an activation claim into a JWT strimg func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) { + return a.EncodeWithSigner(pair, nil) +} + +func (a *ActivationClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) { return "", errors.New("expected subject to be an account") } a.Type = ActivationClaim - return a.ClaimsData.encode(pair, a) + return a.ClaimsData.encode(pair, a, fn) } // DecodeActivationClaims tries to create an activation claim from a JWT string diff --git a/v2/activation_claims_test.go b/v2/activation_claims_test.go index aac9932..ee28c88 100644 --- a/v2/activation_claims_test.go +++ b/v2/activation_claims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -395,3 +395,37 @@ func TestActivationClaimRevocation(t *testing.T) { t.Fatal("account validation shouldn't have failed") } } + +func TestActivationClaimsSignFn(t *testing.T) { + akp := createAccountNKey(t) + target := createAccountNKey(t) + + act := NewActivationClaims(publicKey(target, t)) + act.ImportSubject = "foo" + act.ImportType = Stream + ok := false + s, err := act.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { + ok = true + if pub != publicKey(akp, t) { + t.Fatal("expected pub key to match account") + } + return akp.Sign(data) + }) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("expected ok to be true") + } + + act, err = DecodeActivationClaims(s) + if err != nil { + t.Fatal(err) + } + + vr := CreateValidationResults() + act.Validate(vr) + if !vr.IsEmpty() { + t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) + } +} diff --git a/v2/authorization_claims.go b/v2/authorization_claims.go index fccdcf2..3448f11 100644 --- a/v2/authorization_claims.go +++ b/v2/authorization_claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2022 The NATS Authors + * Copyright 2022-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -113,8 +113,12 @@ func (ac *AuthorizationRequestClaims) Validate(vr *ValidationResults) { // Encode tries to turn the auth request claims into a JWT string. func (ac *AuthorizationRequestClaims) Encode(pair nkeys.KeyPair) (string, error) { + return ac.EncodeWithSigner(pair, nil) +} + +func (ac *AuthorizationRequestClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { ac.Type = AuthorizationRequestClaim - return ac.ClaimsData.encode(pair, ac) + return ac.ClaimsData.encode(pair, ac, fn) } // DecodeAuthorizationRequestClaims tries to parse an auth request claims from a JWT string @@ -242,6 +246,10 @@ func (ar *AuthorizationResponseClaims) Validate(vr *ValidationResults) { // Encode tries to turn the auth request claims into a JWT string. func (ar *AuthorizationResponseClaims) Encode(pair nkeys.KeyPair) (string, error) { + return ar.EncodeWithSigner(pair, nil) +} + +func (ar *AuthorizationResponseClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { ar.Type = AuthorizationResponseClaim - return ar.ClaimsData.encode(pair, ar) + return ar.ClaimsData.encode(pair, ar, fn) } diff --git a/v2/authorization_claims_test.go b/v2/authorization_claims_test.go index 2830345..273dc04 100644 --- a/v2/authorization_claims_test.go +++ b/v2/authorization_claims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2022 The NATS Authors + * Copyright 2022-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -155,3 +155,40 @@ func TestAuthorizationResponse_Decode(t *testing.T) { AssertTrue(nkeys.IsValidPublicUserKey(r.Subject), t) AssertTrue(nkeys.IsValidPublicServerKey(r.Audience), t) } + +func TestNewAuthorizationRequestSignerFn(t *testing.T) { + skp, _ := nkeys.CreateServer() + + kp, err := nkeys.CreateUser() + if err != nil { + t.Fatalf("Error creating user: %v", err) + } + + // the subject of the claim is the user we are generating an authorization response + ac := NewAuthorizationRequestClaims(publicKey(kp, t)) + ac.Server.Name = "NATS-1" + ac.UserNkey = publicKey(kp, t) + + ok := false + ar, err := ac.EncodeWithSigner(skp, func(pub string, data []byte) ([]byte, error) { + ok = true + return skp.Sign(data) + }) + if err != nil { + t.Fatal("error signing request") + } + if !ok { + t.Fatal("not signed by signer function") + } + + ac2, err := DecodeAuthorizationRequestClaims(ar) + if err != nil { + t.Fatal("error decoding authorization request jwt", err) + } + + vr := CreateValidationResults() + ac2.Validate(vr) + if !vr.IsEmpty() { + t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) + } +} diff --git a/v2/claims.go b/v2/claims.go index daac2d8..9b816c3 100644 --- a/v2/claims.go +++ b/v2/claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -68,10 +68,16 @@ func IsGenericClaimType(s string) bool { } } +// SignFn is used in an external sign environment. The function should be +// able to locate the private key for the specified pub key specified and sign the +// specified data returning the signature as generated. +type SignFn func(pub string, data []byte) ([]byte, error) + // Claims is a JWT claims type Claims interface { Claims() *ClaimsData Encode(kp nkeys.KeyPair) (string, error) + EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) ExpectedPrefixes() []nkeys.PrefixByte Payload() interface{} String() string @@ -121,7 +127,7 @@ func serialize(v interface{}) (string, error) { return encodeToString(j), nil } -func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims) (string, error) { +func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims, fn SignFn) (string, error) { if header == nil { return "", errors.New("header is required") } @@ -200,9 +206,21 @@ func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims) (s if header.Algorithm == AlgorithmNkeyOld { return "", errors.New(AlgorithmNkeyOld + " not supported to write jwtV2") } else if header.Algorithm == AlgorithmNkey { - sig, err := kp.Sign([]byte(toSign)) - if err != nil { - return "", err + var sig []byte + if fn != nil { + pk, err := kp.PublicKey() + if err != nil { + return "", err + } + sig, err = fn(pk, []byte(toSign)) + if err != nil { + return "", err + } + } else { + sig, err = kp.Sign([]byte(toSign)) + if err != nil { + return "", err + } } eSig = encodeToString(sig) } else { @@ -224,8 +242,8 @@ func (c *ClaimsData) hash() (string, error) { // Encode encodes a claim into a JWT token. The claim is signed with the // provided nkey's private key -func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims) (string, error) { - return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload) +func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims, fn SignFn) (string, error) { + return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload, fn) } // Returns a JSON representation of the claim diff --git a/v2/decoder_test.go b/v2/decoder_test.go index 020fbac..506c5f5 100644 --- a/v2/decoder_test.go +++ b/v2/decoder_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -68,7 +68,7 @@ func TestBadType(t *testing.T) { c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" - token, err := c.doEncode(&h, kp, c) + token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } @@ -97,7 +97,7 @@ func TestBadAlgo(t *testing.T) { c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" - if _, err := c.doEncode(&h, kp, c); err == nil { + if _, err := c.doEncode(&h, kp, c, nil); err == nil { t.Fatal("expected an error due to bad algorithm") } @@ -105,7 +105,7 @@ func TestBadAlgo(t *testing.T) { c = NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" - if _, err := c.doEncode(&h, kp, c); err == nil { + if _, err := c.doEncode(&h, kp, c, nil); err == nil { t.Fatal("expected an error due to bad algorithm") } } @@ -120,7 +120,7 @@ func TestBadJWT(t *testing.T) { c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" - token, err := c.doEncode(&h, kp, c) + token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } @@ -144,14 +144,14 @@ func TestBadJWT(t *testing.T) { func TestBadSignature(t *testing.T) { kp := createAccountNKey(t) - for algo, error := range map[string]string{ + for algo, anErr := range map[string]string{ AlgorithmNkey: "claim failed V2 signature verification", } { h := Header{TokenTypeJwt, algo} c := NewGenericClaims(publicKey(createUserNKey(t), t)) c.Data["foo"] = "bar" - token, err := c.doEncode(&h, kp, c) + token, err := c.doEncode(&h, kp, c, nil) if err != nil { t.Fatal(err) } @@ -167,7 +167,7 @@ func TestBadSignature(t *testing.T) { t.Fatal("nil error on bad token") } - if err.Error() != error { + if err.Error() != anErr { m := fmt.Sprintf("expected failed signature: %q", err.Error()) t.Fatal(m) } @@ -358,7 +358,9 @@ func TestClaimsStringIsJSON(t *testing.T) { claims.Data["foo"] = "bar" claims2 := NewGenericClaims(publicKey(akp, t)) - json.Unmarshal([]byte(claims.String()), claims2) + if json.Unmarshal([]byte(claims.String()), claims2) != nil { + t.Fatal("failed to unmarshal claims") + } if claims2.Data["foo"] != "bar" { t.Fatalf("Failed to decode expected claim from String representation: %q", claims.String()) } @@ -367,7 +369,7 @@ func TestClaimsStringIsJSON(t *testing.T) { func TestDoEncodeNilHeader(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) - _, err := claims.doEncode(nil, nil, claims) + _, err := claims.doEncode(nil, nil, claims, nil) if err == nil { t.Fatal("should have failed to encode") } @@ -379,7 +381,7 @@ func TestDoEncodeNilHeader(t *testing.T) { func TestDoEncodeNilKeyPair(t *testing.T) { akp := createAccountNKey(t) claims := NewGenericClaims(publicKey(akp, t)) - _, err := claims.doEncode(&Header{}, nil, claims) + _, err := claims.doEncode(&Header{}, nil, claims, nil) if err == nil { t.Fatal("should have failed to encode") } diff --git a/v2/exports.go b/v2/exports.go index 3ebc029..0f26e84 100644 --- a/v2/exports.go +++ b/v2/exports.go @@ -273,7 +273,7 @@ func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) { } // Validate calls validate on all of the exports -func (e *Exports) Validate(vr *ValidationResults) error { +func (e *Exports) Validate(vr *ValidationResults) { var serviceSubjects []Subject var streamSubjects []Subject @@ -292,8 +292,6 @@ func (e *Exports) Validate(vr *ValidationResults) error { isContainedIn(Service, serviceSubjects, vr) isContainedIn(Stream, streamSubjects, vr) - - return nil } // HasExportContainingSubject checks if the export list has an export with the provided subject diff --git a/v2/genericlaims.go b/v2/genericlaims.go index 6793c9e..e680866 100644 --- a/v2/genericlaims.go +++ b/v2/genericlaims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -107,7 +107,11 @@ func (gc *GenericClaims) Payload() interface{} { // Encode takes a generic claims and creates a JWT string func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) { - return gc.ClaimsData.encode(pair, gc) + return gc.ClaimsData.encode(pair, gc, nil) +} + +func (gc *GenericClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { + return gc.ClaimsData.encode(pair, gc, fn) } // Validate checks the generic part of the claims data diff --git a/v2/go.mod b/v2/go.mod index b0d2610..8ca98ca 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -2,7 +2,7 @@ module github.com/nats-io/jwt/v2 go 1.22 -require github.com/nats-io/nkeys v0.4.7 +require github.com/nats-io/nkeys v0.4.8 retract ( v2.7.1 // contains retractions only @@ -10,6 +10,6 @@ retract ( ) require ( - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/sys v0.27.0 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index 4d5a243..13ad73b 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,6 +1,6 @@ -github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= -github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/nats-io/nkeys v0.4.8 h1:+wee30071y3vCZAYRsnrmIPaOe47A/SkK/UBDPdIV70= +github.com/nats-io/nkeys v0.4.8/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/v2/operator_claims.go b/v2/operator_claims.go index 673225f..b5c9c94 100644 --- a/v2/operator_claims.go +++ b/v2/operator_claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -191,6 +191,10 @@ func (oc *OperatorClaims) DidSign(op Claims) bool { // Encode the claims into a JWT string func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { + return oc.EncodeWithSigner(pair, nil) +} + +func (oc *OperatorClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicOperatorKey(oc.Subject) { return "", errors.New("expected subject to be an operator public key") } @@ -199,7 +203,7 @@ func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) { return "", err } oc.Type = OperatorClaim - return oc.ClaimsData.encode(pair, oc) + return oc.ClaimsData.encode(pair, oc, fn) } func (oc *OperatorClaims) ClaimType() ClaimType { diff --git a/v2/operator_claims_test.go b/v2/operator_claims_test.go index dcc4d34..5a6c51b 100644 --- a/v2/operator_claims_test.go +++ b/v2/operator_claims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -502,3 +502,32 @@ func TestOperatorClaims_GetTags(t *testing.T) { t.Fatal("expected tag bar") } } + +func TestNewOperatorClaimSignerFn(t *testing.T) { + kp := createOperatorNKey(t) + + ok := false + oc := NewOperatorClaims(publicKey(kp, t)) + token, err := oc.EncodeWithSigner(kp, func(pub string, data []byte) ([]byte, error) { + ok = true + return kp.Sign(data) + }) + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("expected ok to be true") + } + + oc, err = DecodeOperatorClaims(token) + if err != nil { + t.Fatal("failed to decode", err) + } + + vr := CreateValidationResults() + oc.Validate(vr) + if !vr.IsEmpty() { + t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) + } + +} diff --git a/v2/test/genericclaims_test.go b/v2/test/genericclaims_test.go index b2bbce6..4185b15 100644 --- a/v2/test/genericclaims_test.go +++ b/v2/test/genericclaims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -120,3 +120,35 @@ func TestGenericClaimsCanHaveCustomTypeFromV1(t *testing.T) { t.Fatalf("expected internal type to be 'my_type': %v", gc2.Data["type"]) } } + +func TestGenericClaimsSignerFn(t *testing.T) { + akp := createAccountNKey(t) + apk := publicKey(akp, t) + + gc := NewGenericClaims(apk) + gc.Expires = time.Now().Add(time.Hour).UTC().Unix() + gc.Name = "alberto" + gc.Data["hello"] = "world" + gc.Data["count"] = 5 + gc.Data["type"] = "my_type" + + ok := false + gcJwt, err := gc.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { + ok = true + return akp.Sign(data) + }) + if err != nil { + t.Fatal("failed to encode") + } + if !ok { + t.Fatal("didn't encode with function") + } + + gc2, err := DecodeGeneric(gcJwt) + if err != nil { + t.Fatal("failed to decode", err) + } + if gc2.ClaimType() != GenericClaim { + t.Fatalf("expected claimtype to be generic got: %v", gc2.ClaimType()) + } +} diff --git a/v2/user_claims.go b/v2/user_claims.go index 53b781d..294cc4b 100644 --- a/v2/user_claims.go +++ b/v2/user_claims.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -92,11 +92,15 @@ func (u *UserClaims) HasEmptyPermissions() bool { // Encode tries to turn the user claims into a JWT string func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) { + return u.EncodeWithSigner(pair, nil) +} + +func (u *UserClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) { if !nkeys.IsValidPublicUserKey(u.Subject) { return "", errors.New("expected subject to be user public key") } u.Type = UserClaim - return u.ClaimsData.encode(pair, u) + return u.ClaimsData.encode(pair, u, fn) } // DecodeUserClaims tries to parse a user claims from a JWT string diff --git a/v2/user_claims_test.go b/v2/user_claims_test.go index 2abd028..908a3f3 100644 --- a/v2/user_claims_test.go +++ b/v2/user_claims_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 The NATS Authors + * Copyright 2018-2024 The NATS Authors * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -445,3 +445,36 @@ func TestUserClaims_GetTags(t *testing.T) { t.Fatal("expected tag bar") } } + +func TestUserClaimsSignerFn(t *testing.T) { + akp := createAccountNKey(t) + ukp := createUserNKey(t) + + uc := NewUserClaims(publicKey(ukp, t)) + if !uc.Limits.IsUnlimited() { + t.Fatal("unlimited after creation") + } + + ok := false + tok, err := uc.EncodeWithSigner(akp, func(pub string, data []byte) ([]byte, error) { + ok = true + return akp.Sign(data) + }) + if err != nil { + t.Fatal("error encoding") + } + if !ok { + t.Fatal("fn didn't sign") + } + + uc2, err := DecodeUserClaims(tok) + if err != nil { + t.Fatal("failed to decode uc", err) + } + + vr := CreateValidationResults() + uc2.Validate(vr) + if !vr.IsEmpty() { + t.Fatalf("claims validation should not have failed, got %+v", vr.Issues) + } +}