From 6e5260893633de363646b25d10389ed7608c3c63 Mon Sep 17 00:00:00 2001 From: Matthias Hanel Date: Wed, 29 Jun 2022 18:19:14 +0200 Subject: [PATCH] [added] support for jwt account option DisallowBearer (#3127) * [added] support for jwt account option DisallowBearer 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 update jwt library to 2.3.0 Signed-off-by: Matthias Hanel --- go.mod | 2 +- go.sum | 4 +- server/accounts.go | 25 ++++++++++--- server/auth.go | 4 ++ server/events_test.go | 9 +++-- server/jetstream_jwt_test.go | 71 ++++++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 70eab069..8b0d6610 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/klauspost/compress v1.15.5 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.3.0 github.com/nats-io/nats.go v1.16.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 2a95e06e..4740e2c3 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= +github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/nats.go v1.16.0 h1:zvLE7fGBQYW6MWaFaRdsgm9qT39PJDQoju+DS8KsO1g= github.com/nats-io/nats.go v1.16.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 edf6ea14..e2b24658 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -96,10 +96,11 @@ type Account struct { // Account based limits. type limits struct { - mpay int32 - msubs int32 - mconns int32 - mleafs int32 + mpay int32 + msubs int32 + mconns int32 + mleafs int32 + disallowBearer bool } // Used to track remote clients and leafnodes per remote server. @@ -230,7 +231,7 @@ type importMap struct { func NewAccount(name string) *Account { a := &Account{ Name: name, - limits: limits{-1, -1, -1, -1}, + limits: limits{-1, -1, -1, -1, false}, eventIds: nuid.New(), } return a @@ -2838,6 +2839,13 @@ func (a *Account) checkUserRevoked(nkey string, issuedAt int64) bool { return isRevoked(a.usersRevoked, nkey, issuedAt) } +// failBearer will return if bearer token are allowed (false) or disallowed (true) +func (a *Account) failBearer() bool { + a.mu.RLock() + defer a.mu.RUnlock() + return a.disallowBearer +} + // Check expiration and set the proper state as needed. func (a *Account) checkExpiration(claims *jwt.ClaimsData) { a.mu.Lock() @@ -3242,6 +3250,7 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim a.mpay = int32(ac.Limits.Payload) a.mconns = int32(ac.Limits.Conn) a.mleafs = int32(ac.Limits.LeafNodeConn) + a.disallowBearer = ac.Limits.DisallowBearer // Check for any revocations if len(ac.Revocations) > 0 { // We will always replace whatever we had with most current, so no @@ -3344,11 +3353,15 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim theJWT := c.opts.JWT c.mu.Unlock() // Check for being revoked here. We use ac one to avoid the account lock. - if ac.Revocations != nil && theJWT != _EMPTY_ { + if (ac.Revocations != nil || ac.Limits.DisallowBearer) && theJWT != _EMPTY_ { if juc, err := jwt.DecodeUserClaims(theJWT); err != nil { c.Debugf("User JWT not valid: %v", err) c.authViolation() continue + } else if juc.BearerToken && ac.Limits.DisallowBearer { + c.Debugf("Bearer User JWT not allowed") + c.authViolation() + continue } else if ok := ac.IsClaimRevoked(juc); ok { c.sendErrAndDebug("User Authentication Revoked") c.closeConnection(Revocation) diff --git a/server/auth.go b/server/auth.go index e7b4d56a..d2295c39 100644 --- a/server/auth.go +++ b/server/auth.go @@ -629,6 +629,10 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo c.Debugf("Account JWT has expired") return false } + if juc.BearerToken && acc.failBearer() { + c.Debugf("Account does not allow bearer token") + return false + } // skip validation of nonce when presented with a bearer token // FIXME: if BearerToken is only for WSS, need check for server with that port enabled if !juc.BearerToken { diff --git a/server/events_test.go b/server/events_test.go index 5445808e..35b1c38e 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/jetstream_jwt_test.go b/server/jetstream_jwt_test.go index 45ec44fb..a14dfb62 100644 --- a/server/jetstream_jwt_test.go +++ b/server/jetstream_jwt_test.go @@ -210,6 +210,77 @@ func TestJetStreamJWTLimits(t *testing.T) { c.Close() } +func TestJetStreamJWTDisallowBearer(t *testing.T) { + sysKp, syspub := createKey(t) + sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub) + sysCreds := newUser(t, sysKp) + defer removeFile(t, sysCreds) + + accKp, err := nkeys.CreateAccount() + require_NoError(t, err) + accIdPub, err := accKp.PublicKey() + require_NoError(t, err) + aClaim := jwt.NewAccountClaims(accIdPub) + accJwt1, err := aClaim.Encode(oKp) + require_NoError(t, err) + aClaim.Limits.DisallowBearer = true + accJwt2, err := aClaim.Encode(oKp) + require_NoError(t, err) + + uc := jwt.NewUserClaims("dummy") + uc.BearerToken = true + uOpt1 := createUserCredsEx(t, uc, accKp) + uc.BearerToken = false + uOpt2 := createUserCredsEx(t, uc, accKp) + + dir := createDir(t, "srv") + defer removeDir(t, dir) + cf := createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + operator = %s + system_account: %s + resolver: { + type: full + dir: '%s/jwt' + } + resolver_preload = { + %s : "%s" + } + `, ojwt, syspub, dir, syspub, sysJwt))) + defer removeFile(t, cf) + s, _ := RunServerWithConfig(cf) + defer s.Shutdown() + + updateJwt(t, s.ClientURL(), sysCreds, accJwt1, 1) + disconnectErrCh := make(chan error, 10) + defer close(disconnectErrCh) + nc1, err := nats.Connect(s.ClientURL(), uOpt1, + nats.NoReconnect(), + nats.ErrorHandler(func(conn *nats.Conn, s *nats.Subscription, err error) { + disconnectErrCh <- err + })) + require_NoError(t, err) + defer nc1.Close() + + // update jwt and observe bearer token get disconnected + updateJwt(t, s.ClientURL(), sysCreds, accJwt2, 1) + select { + case err := <-disconnectErrCh: + require_Contains(t, err.Error(), "authorization violation") + case <-time.After(time.Second): + t.Fatalf("expected error on disconnect") + } + + // assure bearer token is not allowed to connect + _, err = nats.Connect(s.ClientURL(), uOpt1) + require_Error(t, err) + + // assure non bearer token can connect + nc2, err := nats.Connect(s.ClientURL(), uOpt2) + require_NoError(t, err) + defer nc2.Close() +} + func TestJetStreamJWTMove(t *testing.T) { sysKp, syspub := createKey(t) sysJwt := encodeClaim(t, jwt.NewAccountClaims(syspub), syspub)