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

Issue:
Account Policy to Restrict Bearer Token Users
## Feature Request

In operator-mode configurations (JWT-based), an account owner can create bearer token User JWTs using the `--bearer` option of `nsc` or the the JWT client libraries.

Some operators may desire to create accounts that explicitly restrict the account owner from creating users with the bearer-token option.
                
#### Use Case:

Force client to present User JWT **and** perform a challenge-and-response authentication, i.e. sign nonce using possessed private NKEY. 

#### Proposed Change:

Add an account-level claim that asserts that the account may not issue bearer User JWTs.

#### Who Benefits From The Change(s)?

Organizations/operators that have a policy against bearer token usage at runtime, but otherwise enable account owners to manage their own user lifecycle.

#### Alternative Approaches



Pull request:
[added] support for jwt operator option DisallowBearerToken
I modified an existing data structure that held a similar attribute already.
Instead this data structure references the claim.

change 3 out of 3. Fixes #3084
corresponds to:
https://github.com/nats-io/jwt/pull/177
https://github.com/nats-io/nsc/pull/495

Signed-off-by: Matthias Hanel <mh@synadia.com>


Patch:
diff --git a/go.mod b/go.mod
index 07408231..753893f6 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.17
 require (
 	github.com/klauspost/compress v1.14.4
 	github.com/minio/highwayhash v1.0.2
-	github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a
+	github.com/nats-io/jwt/v2 v2.2.1-0.20220429175140-9f4f1d5cc977
 	github.com/nats-io/nats.go v1.14.0
 	github.com/nats-io/nkeys v0.3.0
 	github.com/nats-io/nuid v1.0.1
diff --git a/go.sum b/go.sum
index 81e28446..fe34d7f2 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6
 github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
 github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
-github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a h1:lem6QCvxR0Y28gth9P+wV2K/zYUUAkJ+55U8cpS0p5I=
-github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
+github.com/nats-io/jwt/v2 v2.2.1-0.20220429175140-9f4f1d5cc977 h1:cbP9xTzFVGWXCJtApKchvFZIIEDWKfLkThVYLonpOpQ=
+github.com/nats-io/jwt/v2 v2.2.1-0.20220429175140-9f4f1d5cc977/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=
 github.com/nats-io/nats.go v1.14.0 h1:/QLCss4vQ6wvDpbqXucsVRDi13tFIR6kTdau+nXzKJw=
 github.com/nats-io/nats.go v1.14.0/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=
 github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
diff --git a/server/accounts.go b/server/accounts.go
index d1333955..cde23482 100644
--- a/server/accounts.go
+++ b/server/accounts.go
@@ -2983,7 +2983,8 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
 
 	// update account signing keys
 	a.signingKeys = nil
-	_, strict := s.strictSigningKeyUsage[a.Issuer]
+	op, ok := s.keyToClaim[a.Issuer]
+	strict := ok && op.StrictSigningKeyUsage
 	if len(ac.SigningKeys) > 0 || !strict {
 		a.signingKeys = make(map[string]jwt.Scope)
 	}
diff --git a/server/auth.go b/server/auth.go
index e7b4d56a..f3a68350 100644
--- a/server/auth.go
+++ b/server/auth.go
@@ -607,9 +607,12 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo
 			c.Debugf("Account JWT lookup error: %v", err)
 			return false
 		}
-		if !s.isTrustedIssuer(acc.Issuer) {
+		if isTrusted, noBearer := s.isTrustedIssuer(acc.Issuer); !isTrusted {
 			c.Debugf("Account JWT not signed by trusted operator")
 			return false
+		} else if noBearer && juc.BearerToken {
+			c.Debugf("Bearer Token are disallowed by operator")
+			return false
 		}
 		if scope, ok := acc.hasIssuer(juc.Issuer); !ok {
 			c.Debugf("User JWT issuer is not known")
diff --git a/server/server.go b/server/server.go
index 1e97a16d..36a3de56 100644
--- a/server/server.go
+++ b/server/server.go
@@ -209,8 +209,8 @@ type Server struct {
 
 	// Trusted public operator keys.
 	trustedKeys []string
-	// map of trusted keys to operator setting StrictSigningKeyUsage
-	strictSigningKeyUsage map[string]struct{}
+	// map of trusted keys to operator claims
+	keyToClaim map[string]*jwt.OperatorClaims
 
 	// We use this to minimize mem copies for requests to monitoring
 	// endpoint /varz (when it comes from http).
@@ -970,27 +970,32 @@ func (s *Server) ActivePeers() (peers []string) {
 	return peers
 }
 
-// isTrustedIssuer will check that the issuer is a trusted public key.
+// isTrustedIssuer will check that the issuer is a trusted public key,
+// and if true, will also return if the operator disallows use of bearer token.
 // This is used to make sure an account was signed by a trusted operator.
-func (s *Server) isTrustedIssuer(issuer string) bool {
+// If the account is not trusted, disallow bearer token will always be false.
+func (s *Server) isTrustedIssuer(issuer string) (bool, bool) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	// If we are not running in trusted mode and there is no issuer, that is ok.
 	if s.trustedKeys == nil && issuer == "" {
-		return true
+		return true, false
 	}
 	for _, tk := range s.trustedKeys {
 		if tk == issuer {
-			return true
+			if op, ok := s.keyToClaim[issuer]; ok {
+				return true, op.DisallowBearerToken
+			}
+			return true, false
 		}
 	}
-	return false
+	return false, false
 }
 
 // processTrustedKeys will process binary stamped and
 // options-based trusted nkeys. Returns success.
 func (s *Server) processTrustedKeys() bool {
-	s.strictSigningKeyUsage = map[string]struct{}{}
+	s.keyToClaim = map[string]*jwt.OperatorClaims{}
 	if trustedKeys != "" && !s.initStampedTrustedKeys() {
 		return false
 	} else if s.opts.TrustedKeys != nil {
@@ -1001,11 +1006,9 @@ func (s *Server) processTrustedKeys() bool {
 		}
 		s.trustedKeys = append([]string(nil), s.opts.TrustedKeys...)
 		for _, claim := range s.opts.TrustedOperators {
-			if !claim.StrictSigningKeyUsage {
-				continue
-			}
+			s.keyToClaim[claim.Subject] = claim
 			for _, key := range claim.SigningKeys {
-				s.strictSigningKeyUsage[key] = struct{}{}
+				s.keyToClaim[key] = claim
 			}
 		}
 	}
@@ -1222,7 +1225,7 @@ func (s *Server) setSystemAccount(acc *Account) error {
 	}
 	// If we are running with trusted keys for an operator
 	// make sure we check the account is legit.
-	if !s.isTrustedIssuer(acc.Issuer) {
+	if isTrusted, _ := s.isTrustedIssuer(acc.Issuer); !isTrusted {
 		return ErrAccountValidation
 	}
 
@@ -1542,7 +1545,7 @@ func (s *Server) verifyAccountClaims(claimJWT string) (*jwt.AccountClaims, strin
 	if err != nil {
 		return nil, _EMPTY_, err
 	}
-	if !s.isTrustedIssuer(accClaims.Issuer) {
+	if isTrusted, _ := s.isTrustedIssuer(accClaims.Issuer); !isTrusted {
 		return nil, _EMPTY_, ErrAccountValidation
 	}
 	vr := jwt.CreateValidationResults()


Test patch:
diff --git a/server/events_test.go b/server/events_test.go
index e2f12b51..ced4c9d3 100644
--- a/server/events_test.go
+++ b/server/events_test.go
@@ -45,11 +45,10 @@ func createAccount(s *Server) (*Account, nkeys.KeyPair) {
 	return acc, akp
 }
 
-func createUserCreds(t *testing.T, s *Server, akp nkeys.KeyPair) nats.Option {
+func createUserCredsEx(t *testing.T, nuc *jwt.UserClaims, akp nkeys.KeyPair) nats.Option {
 	t.Helper()
 	kp, _ := nkeys.CreateUser()
-	pub, _ := kp.PublicKey()
-	nuc := jwt.NewUserClaims(pub)
+	nuc.Subject, _ = kp.PublicKey()
 	ujwt, err := nuc.Encode(akp)
 	if err != nil {
 		t.Fatalf("Error generating user JWT: %v", err)
@@ -64,6 +63,10 @@ func createUserCreds(t *testing.T, s *Server, akp nkeys.KeyPair) nats.Option {
 	return nats.UserJWT(userCB, sigCB)
 }
 
+func createUserCreds(t *testing.T, s *Server, akp nkeys.KeyPair) nats.Option {
+	return createUserCredsEx(t, jwt.NewUserClaims("test"), akp)
+}
+
 func runTrustedServer(t *testing.T) (*Server, *Options) {
 	t.Helper()
 	opts := DefaultOptions()
diff --git a/server/jwt_test.go b/server/jwt_test.go
index a148ecf1..7c5c0329 100644
--- a/server/jwt_test.go
+++ b/server/jwt_test.go
@@ -5760,6 +5760,50 @@ func TestJWTStrictSigningKeys(t *testing.T) {
 	})
 }
 
+func TestJWTDisallowBearer(t *testing.T) {
+	opId, err := nkeys.CreateOperator()
+	require_NoError(t, err)
+	opIdPub, err := opId.PublicKey()
+	require_NoError(t, err)
+	oClaim := jwt.NewOperatorClaims(opIdPub)
+	oClaim.DisallowBearerToken = true
+	oJwt, err := oClaim.Encode(opId)
+	require_NoError(t, err)
+
+	accId, err := nkeys.CreateAccount()
+	require_NoError(t, err)
+	accIdPub, err := accId.PublicKey()
+	require_NoError(t, err)
+	aClaim := jwt.NewAccountClaims(accIdPub)
+	accJwt, err := aClaim.Encode(opId)
+	require_NoError(t, err)
+
+	uc := jwt.NewUserClaims("dummy")
+	uc.BearerToken = true
+	uOpt1 := createUserCredsEx(t, uc, accId)
+	uc.BearerToken = false
+	uOpt2 := createUserCredsEx(t, uc, accId)
+
+	cf := createConfFile(t, []byte(fmt.Sprintf(`
+		port: -1
+		operator = %s
+		resolver: MEMORY
+		resolver_preload = {
+			%s : "%s"
+		}
+		`, oJwt, accIdPub, accJwt)))
+	defer removeFile(t, cf)
+	s, _ := RunServerWithConfig(cf)
+	defer s.Shutdown()
+
+	_, err = nats.Connect(s.ClientURL(), uOpt1)
+	require_Error(t, err)
+
+	nc2, err := nats.Connect(s.ClientURL(), uOpt2)
+	require_NoError(t, err)
+	defer nc2.Close()
+}
+
 func TestJWTAccountProtectedImport(t *testing.T) {
 	srvFmt := `
 		port: -1