From bcfeb5a7cea0462df8234d9f0068495399c4db53 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Tue, 26 Nov 2024 16:55:27 -0600 Subject: [PATCH 1/2] feat: Claims interface now has `EncodeWithSigner(nkeys.KeyPair`, fn: SignFn)` where signing can be delegated to a function. The `type SignFn func(pub string, data []byte) ([]byte, error)` is provided with the public key whose matching private key should be used to sign the provided payload. This feature enables an external signing service to be incorporated in the workflow for signing a JWT. Signed-off-by: Alberto Ricart --- .github/workflows/go-test.yaml | 6 ++--- v2/account_claims.go | 18 ++++++++------ v2/account_claims_test.go | 42 ++++++++++++++++++++++++++++++++- v2/activation_claims.go | 8 +++++-- v2/activation_claims_test.go | 36 +++++++++++++++++++++++++++- v2/authorization_claims.go | 14 ++++++++--- v2/authorization_claims_test.go | 39 +++++++++++++++++++++++++++++- v2/claims.go | 32 +++++++++++++++++++------ v2/decoder_test.go | 24 ++++++++++--------- v2/exports.go | 4 +--- v2/genericlaims.go | 8 +++++-- v2/operator_claims.go | 8 +++++-- v2/operator_claims_test.go | 31 +++++++++++++++++++++++- v2/test/genericclaims_test.go | 34 +++++++++++++++++++++++++- v2/user_claims.go | 8 +++++-- v2/user_claims_test.go | 35 ++++++++++++++++++++++++++- 16 files changed, 299 insertions(+), 48 deletions(-) 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/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) + } +} From 5297029e97860e4fa3290db072175f31c461fd75 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Tue, 26 Nov 2024 17:48:03 -0600 Subject: [PATCH 2/2] go mod Signed-off-by: Alberto Ricart --- v2/go.mod | 6 +++--- v2/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) 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=