Repository: nats-io/nats-server (go)

Issue:
sample config with leafnode and nkeys
Is it possible to add to docs sample config with leaf nodes secured by nkey?
I have found ugly solution using credential file but it require pretense of JWT token. 
I kept JWT token from docs and put correct USER NKEY SEED there.
Is there better solution?

on leafnode `nats.conf`
```
leafnodes {
    remotes = [
        {
           url: "nats-leaf://@server_url:7422"
          account: someuser
         credentials: "server.creds"
        }
    ]
}
```
where `server.creds`
is copy of example creds from docs
```
**** this part is keept as it is in docs ****
-----BEGIN NATS USER JWT-----
eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJUVlNNTEtTWkJBN01VWDNYQUxNUVQzTjRISUw1UkZGQU9YNUtaUFhEU0oyWlAzNkVMNVJBIiwiaWF0IjoxNTU4MDQ1NTYyLCJpc3MiOiJBQlZTQk0zVTQ1REdZRVVFQ0tYUVM3QkVOSFdHN0tGUVVEUlRFSEFKQVNPUlBWV0JaNEhPSUtDSCIsIm5hbWUiOiJvbWVnYSIsInN1YiI6IlVEWEIyVk1MWFBBU0FKN1pEVEtZTlE3UU9DRldTR0I0Rk9NWVFRMjVIUVdTQUY3WlFKRUJTUVNXIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.6TQ2ilCDb6m2ZDiJuj_D_OePGXFyN3Ap2DEm3ipcU5AhrWrNvneJryWrpgi_yuVWKo1UoD5s8bxlmwypWVGFAA
------END NATS USER JWT------

************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.

-----BEGIN USER NKEY SEED-----
REAL_USER_PRIVATE_KEY
------END USER NKEY SEED------

*************************************************************
```

Server config for reference:
```
leafnodes {
  port: 7422
  authorization: {
        users: [ {user: UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4,  account: someuser}]
  }
}
```

Pull request description:
[IMPROVED] Allow no_auth_user to be an nkey.
Partially resolves #3196 

Signed-off-by: Derek Collison <derek@nats.io>

Patch:
diff --git a/server/auth.go b/server/auth.go
index 7a0f93e2..e8bd8f25 100644
--- a/server/auth.go
+++ b/server/auth.go
@@ -1,4 +1,4 @@
-// Copyright 2012-2023 The NATS Authors
+// Copyright 2012-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
@@ -743,13 +743,24 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
 	// Check if we have nkeys or users for client.
 	hasNkeys := len(s.nkeys) > 0
 	hasUsers := len(s.users) > 0
-	if hasNkeys && c.opts.Nkey != _EMPTY_ {
-		nkey, ok = s.nkeys[c.opts.Nkey]
-		if !ok || !c.connectionTypeAllowed(nkey.AllowedConnectionTypes) {
-			s.mu.Unlock()
-			return false
+	if hasNkeys {
+		if (c.kind == CLIENT || c.kind == LEAF) && noAuthUser != _EMPTY_ &&
+			c.opts.Username == _EMPTY_ && c.opts.Password == _EMPTY_ && c.opts.Token == _EMPTY_ && c.opts.Nkey == _EMPTY_ {
+			if _, exists := s.nkeys[noAuthUser]; exists {
+				c.mu.Lock()
+				c.opts.Nkey = noAuthUser
+				c.mu.Unlock()
+			}
+		}
+		if c.opts.Nkey != _EMPTY_ {
+			nkey, ok = s.nkeys[c.opts.Nkey]
+			if !ok || !c.connectionTypeAllowed(nkey.AllowedConnectionTypes) {
+				s.mu.Unlock()
+				return false
+			}
 		}
-	} else if hasUsers {
+	}
+	if hasUsers && nkey == nil {
 		// Check if we are tls verify and are mapping users from the client_certificate.
 		if tlsMap {
 			authorized := checkClientTLSCertSubject(c, func(u string, certDN *ldap.DN, _ bool) (string, bool) {
@@ -989,27 +1000,30 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
 	}
 
 	if nkey != nil {
-		if c.opts.Sig == _EMPTY_ {
-			c.Debugf("Signature missing")
-			return false
-		}
-		sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
-		if err != nil {
-			// Allow fallback to normal base64.
-			sig, err = base64.StdEncoding.DecodeString(c.opts.Sig)
+		// If we did not match noAuthUser check signature which is required.
+		if nkey.Nkey != noAuthUser {
+			if c.opts.Sig == _EMPTY_ {
+				c.Debugf("Signature missing")
+				return false
+			}
+			sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
 			if err != nil {
-				c.Debugf("Signature not valid base64")
+				// Allow fallback to normal base64.
+				sig, err = base64.StdEncoding.DecodeString(c.opts.Sig)
+				if err != nil {
+					c.Debugf("Signature not valid base64")
+					return false
+				}
+			}
+			pub, err := nkeys.FromPublicKey(c.opts.Nkey)
+			if err != nil {
+				c.Debugf("User nkey not valid: %v", err)
+				return false
+			}
+			if err := pub.Verify(c.nonce, sig); err != nil {
+				c.Debugf("Signature not verified")
 				return false
 			}
-		}
-		pub, err := nkeys.FromPublicKey(c.opts.Nkey)
-		if err != nil {
-			c.Debugf("User nkey not valid: %v", err)
-			return false
-		}
-		if err := pub.Verify(c.nonce, sig); err != nil {
-			c.Debugf("Signature not verified")
-			return false
 		}
 		if err := c.RegisterNkeyUser(nkey); err != nil {
 			return false
@@ -1425,15 +1439,21 @@ func validateNoAuthUser(o *Options, noAuthUser string) error {
 	if len(o.TrustedOperators) > 0 {
 		return fmt.Errorf("no_auth_user not compatible with Trusted Operator")
 	}
-	if o.Users == nil {
-		return fmt.Errorf(`no_auth_user: "%s" present, but users are not defined`, noAuthUser)
+
+	if o.Nkeys == nil && o.Users == nil {
+		return fmt.Errorf(`no_auth_user: "%s" present, but users/nkeys are not defined`, noAuthUser)
 	}
 	for _, u := range o.Users {
 		if u.Username == noAuthUser {
 			return nil
 		}
 	}
+	for _, u := range o.Nkeys {
+		if u.Nkey == noAuthUser {
+			return nil
+		}
+	}
 	return fmt.Errorf(
-		`no_auth_user: "%s" not present as user in authorization block or account configuration`,
+		`no_auth_user: "%s" not present as user or nkey in authorization block or account configuration`,
 		noAuthUser)
 }
diff --git a/server/stream.go b/server/stream.go
index 1d47b42e..2e18dc62 100644
--- a/server/stream.go
+++ b/server/stream.go
@@ -1773,8 +1773,6 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool)
 		// Check for Sources.
 		if len(cfg.Sources) > 0 || len(ocfg.Sources) > 0 {
 			currentIName := make(map[string]struct{})
-			needsStartingSeqNum := make(map[string]struct{})
-
 			for _, s := range ocfg.Sources {
 				currentIName[s.iname] = struct{}{}
 			}
@@ -1808,25 +1806,18 @@ func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool)
 					}
 
 					mset.sources[s.iname] = si
-					needsStartingSeqNum[s.iname] = struct{}{}
+					mset.setStartingSequenceForSource(s.iname, s.External)
+					mset.setSourceConsumer(s.iname, si.sseq+1, time.Time{})
 				} else {
 					// source already exists
 					delete(currentIName, s.iname)
 				}
 			}
-			// What is left in currentIName needs to be deleted.
+			// What is left in cuurentIName needs to be deleted.
 			for iName := range currentIName {
 				mset.cancelSourceConsumer(iName)
 				delete(mset.sources, iName)
 			}
-			neededCopy := make(map[string]struct{}, len(needsStartingSeqNum))
-			for iName := range needsStartingSeqNum {
-				neededCopy[iName] = struct{}{}
-			}
-			mset.setStartingSequenceForSources(needsStartingSeqNum)
-			for iName := range neededCopy {
-				mset.setSourceConsumer(iName, mset.sources[iName].sseq+1, time.Time{})
-			}
 		}
 	}
 
@@ -2727,10 +2718,7 @@ func (mset *stream) retrySourceConsumer(iName string) {
 	}
 	var ss = mset.streamSource(iName)
 	if ss != nil {
-		iNameMap := map[string]struct{}{
-			iName: {},
-		}
-		mset.setStartingSequenceForSources(iNameMap)
+		mset.setStartingSequenceForSource(iName, ss.External)
 		mset.retrySourceConsumerAtSeq(iName, si.sseq+1)
 	}
 }
@@ -3346,20 +3334,18 @@ func streamAndSeq(shdr string) (string, string, uint64) {
 }
 
 // Lock should be held.
-func (mset *stream) setStartingSequenceForSources(iNames map[string]struct{}) {
+func (mset *stream) setStartingSequenceForSource(iName string, external *ExternalStream) {
+	si := mset.sources[iName]
+	if si == nil {
+		return
+	}
+
 	var state StreamState
 	mset.store.FastState(&state)
 
 	// Do not reset sseq here so we can remember when purge/expiration happens.
 	if state.Msgs == 0 {
-		for iName := range iNames {
-			si := mset.sources[iName]
-			if si == nil {
-				continue
-			} else {
-				si.dseq = 0
-			}
-		}
+		si.dseq = 0
 		return
 	}
 
@@ -3374,25 +3360,10 @@ func (mset *stream) setStartingSequenceForSources(iNames map[string]struct{}) {
 			continue
 		}
 		streamName, indexName, sseq := streamAndSeq(string(ss))
-
-		if _, ok := iNames[indexName]; ok {
-			si := mset.sources[indexName]
+		if indexName == si.iname || (indexName == _EMPTY_ && (streamName == si.name || (external != nil && streamName == si.name+":"+getHash(external.ApiPrefix)))) {
 			si.sseq = sseq
 			si.dseq = 0
-			delete(iNames, indexName)
-		} else if indexName == _EMPTY_ && streamName != _EMPTY_ {
-			for iName := range iNames {
-				if streamName == mset.sources[iName].name ||
-					(mset.streamSource(iName).External != nil && streamName == mset.sources[iName].name+":"+getHash(mset.streamSource(iName).External.ApiPrefix)) {
-					si := mset.sources[indexName]
-					si.sseq = sseq
-					si.dseq = 0
-					break
-				}
-			}
-		}
-		if len(iNames) == 0 {
-			break
+			return
 		}
 	}
 }


Test patch:
diff --git a/server/auth_test.go b/server/auth_test.go
index 45f92d40..15720144 100644
--- a/server/auth_test.go
+++ b/server/auth_test.go
@@ -1,4 +1,4 @@
-// Copyright 2012-2022 The NATS Authors
+// Copyright 2012-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
@@ -15,6 +15,7 @@ package server
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"net"
 	"net/url"
@@ -277,6 +278,30 @@ func TestNoAuthUser(t *testing.T) {
 	}
 }
 
+func TestNoAuthUserNkey(t *testing.T) {
+	conf := createConfFile(t, []byte(`
+		listen: "127.0.0.1:-1"
+		accounts {
+			FOO { users [{user: "foo", password: "pwd1"}] }
+			BAR { users [{nkey: "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON"}] }
+		}
+		no_auth_user: "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON"
+	`))
+	s, _ := RunServerWithConfig(conf)
+	defer s.Shutdown()
+
+	// Make sure we connect ok and to the correct account.
+	nc := natsConnect(t, s.ClientURL())
+	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
+	require_NoError(t, err)
+	response := ServerAPIResponse{Data: &UserInfo{}}
+	err = json.Unmarshal(resp.Data, &response)
+	require_NoError(t, err)
+	userInfo := response.Data.(*UserInfo)
+	require_Equal(t, userInfo.UserID, "UBO2MQV67TQTVIRV3XFTEZOACM4WLOCMCDMAWN5QVN5PI2N6JHTVDRON")
+	require_Equal(t, userInfo.Account, "BAR")
+}
+
 func TestUserConnectionDeadline(t *testing.T) {
 	clientAuth := &DummyAuth{
 		t:        t,
diff --git a/server/norace_test.go b/server/norace_test.go
index 2866d054..a9f450db 100644
--- a/server/norace_test.go
+++ b/server/norace_test.go
@@ -6647,7 +6647,7 @@ func TestNoRaceJetStreamClusterGhostConsumers(t *testing.T) {
 	})
 }
 
-// This is to test a publish slowdown and general instability experienced in a setup similar to this.
+// This is to test a publish slowdown and general instability experienced in a setup simular to this.
 // We have feeder streams that are all sourced to an aggregate stream. All streams are interest retention.
 // We want to monitor the avg publish time for the sync publishers to the feeder streams, the ingest rate to
 // the aggregate stream, and general health of the consumers on the aggregate stream.
diff --git a/server/opts_test.go b/server/opts_test.go
index 78920de6..de9b21ff 100644
--- a/server/opts_test.go
+++ b/server/opts_test.go
@@ -2890,7 +2890,7 @@ func TestNoAuthUserCode(t *testing.T) {
 		})
 	}
 
-	for _, badUser := range []string{"notthere", "UBAAQWTW6CG2G6ANGNKB5U2B7HRWHSGMZEZX3AQSAJOQDAUGJD46LD2E"} {
+	for _, badUser := range []string{"notthere", "UBAAQWTW6CG2G6ANGNKB5U2B7HRWHSGMZEZX3AQSAJOQDAUGJD46LD2F"} {
 		t.Run(badUser, func(t *testing.T) {
 			os.Setenv("NO_AUTH_USER", badUser)
 			opts, err := ProcessConfigFile(confFileName)