Repository: nats-io/nats-server
Pull request #3864 (2023-02-10 20:58:57)

Simplify authresponse
This PR reworks the auth callout to simply return an user JWT.

 - [X] Tests added
 - [ ] Branch rebased on top of current main (`git pull --rebase origin main`)
 - [X] Changes squashed to a single commit (described [here](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html))
 - [X] Build is green in Travis CI
 - [X] You have certified that the contribution is your original work and that you license the work to the project under the [Apache 2 license](https://github.com/nats-io/nats-server/blob/main/LICENSE)

### Changes proposed in this pull request:

The original auth callout response, is simpler:
- the response is just JSON `{error: string, user_token: string}`. If encrypted the entire payload is encrypted.
- The server id which was set as an audience, is now set as a tag, this allows the server to verify the server (albeit by turning the server ID lower case)
- For operator mode, the callout should include it's public key, and sign the payload, and return both of these values in a header. This way the server can verify that the payload was sent from the expected service. On encrypted payloads this is not required, since the payload is encrypted and the server can verify. 

The callout service can now use account signing keys and scoped signing keys. The server will validate the signing keys used to match the target account, and in the case of the scoped signing key, will assign limits as recorded in the destination account. The one requirement is that the limits be default (zero) struct (otherwise the resulting JWT will be invalid once validation runs).

The audience field is now reserved for account placement on non-operator mode. In operator mode, the user JWT has all the proper attributions to determine the target account.


/cc @nats-io/core



Patch:
diff --git a/go.mod b/go.mod
index 55bf8998..821bd2cc 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,15 @@ module github.com/nats-io/nats-server/v2
 go 1.19
 
 require (
-	github.com/klauspost/compress v1.16.0
+	github.com/klauspost/compress v1.16.3
 	github.com/minio/highwayhash v1.0.2
-	github.com/nats-io/jwt/v2 v2.3.1-0.20221227170542-bdf40fa3627b
+	github.com/nats-io/jwt/v2 v2.4.0
 	github.com/nats-io/nats.go v1.24.0
-	github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546
+	github.com/nats-io/nkeys v0.4.4
 	github.com/nats-io/nuid v1.0.1
 	go.uber.org/automaxprocs v1.5.1
-	golang.org/x/crypto v0.6.0
-	golang.org/x/sys v0.5.0
+	golang.org/x/crypto v0.7.0
+	golang.org/x/sys v0.6.0
 	golang.org/x/time v0.3.0
 )
 
diff --git a/go.sum b/go.sum
index ed8f6b6f..c8ffa644 100644
--- a/go.sum
+++ b/go.sum
@@ -11,14 +11,16 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
 github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
+github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 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.3.1-0.20221227170542-bdf40fa3627b h1:exHeHbghpBp1JvdYq7muaKFvJgLD93UDcmoIbFu/9PA=
-github.com/nats-io/jwt/v2 v2.3.1-0.20221227170542-bdf40fa3627b/go.mod h1:DYujvzCMZzUuqB3i1Pnpf1YtkuTwhdI84Aah9wRXkK0=
+github.com/nats-io/jwt/v2 v2.4.0 h1:1woVcq37qhNwJOeZ4ZoRy5NJU5bvbtGsIammf2GpuJQ=
+github.com/nats-io/jwt/v2 v2.4.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
 github.com/nats-io/nats.go v1.24.0 h1:CRiD8L5GOQu/DcfkmgBcTTIQORMwizF+rPk6T0RaHVQ=
 github.com/nats-io/nats.go v1.24.0/go.mod h1:dVQF+BK3SzUZpwyzHedXsvH3EO38aVKuOPkkHlv5hXA=
-github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546 h1:7ZylVLLiDSFqxJTSibNsO2RjVSXj3QWnDc+zKara2HE=
-github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546/go.mod h1:JOEZlxMfMnmaLwr+mpmP+RGIYSxLNBFsZykCGaI2PvA=
+github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
+github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -26,11 +28,11 @@ github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4
 github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
 go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/server/auth_callout.go b/server/auth_callout.go
index 091bf6ad..6d243d79 100644
--- a/server/auth_callout.go
+++ b/server/auth_callout.go
@@ -17,6 +17,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"time"
 
@@ -34,6 +35,7 @@ const (
 func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
 	isOperatorMode := len(opts.TrustedKeys) > 0
 
+	// this is the account the user connected in, or the one running the callout
 	var acc *Account
 	if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
 		aname := opts.AuthCallout.Account
@@ -63,6 +65,9 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 		xkp, xkey = s.xkp, s.info.XKey
 	}
 
+	// FIXME: so things like the server ID that get assigned, are used as a sort of nonce - but
+	//  reality is that the keypair here, is generated, so the response generated a JWT has to be
+	//  this user - no replay possible
 	// Create a keypair for the user. We will expect this public user to be in the signed response.
 	// This prevents replay attacks.
 	ukp, _ := nkeys.CreateUser()
@@ -71,43 +76,73 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 	reply := s.newRespInbox()
 	respCh := make(chan string, 1)
 
-	processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
+	decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
+		account := acc.Name
 		_, msg := rc.msgParts(rmsg)
+
 		// This signals not authorized.
 		// Since this is an account subscription will always have "\r\n".
 		if len(msg) <= LEN_CR_LF {
-			respCh <- fmt.Sprintf("Auth callout violation: %q on account %q", "no reason supplied", racc.Name)
-			return
+			return nil, fmt.Errorf("Auth callout violation: %q on account %q", "no reason supplied", account)
 		}
 		// Strip trailing CRLF.
 		msg = msg[:len(msg)-LEN_CR_LF]
-
+		encrypted := false
 		// If we sent an encrypted request the response could be encrypted as well.
-		if xkp != nil && len(msg) > len(jwtPrefix) && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
+		// we are expecting the input to be `eyJ` if it is a JWT
+		if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
 			var err error
 			msg, err = xkp.Open(msg, pubAccXKey)
 			if err != nil {
-				respCh <- fmt.Sprintf("Error decrypting auth callout response on account %q: %v", racc.Name, err)
-				return
+				return nil, fmt.Errorf("Error decrypting auth callout response on account %q: %v", account, err)
 			}
+			encrypted = true
 		}
 
-		arc, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
+		cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
 		if err != nil {
-			respCh <- fmt.Sprintf("Error decoding auth callout response on account %q: %v", racc.Name, err)
-			return
+			return nil, err
+		}
+		vr := jwt.CreateValidationResults()
+		cr.Validate(vr)
+		if len(vr.Issues) > 0 {
+			return nil, fmt.Errorf("Authorization response had validation errors: %v", vr.Issues[0])
+		}
+
+		// the subject is the user id
+		if cr.Subject != pub {
+			return nil, errors.New("Auth callout violation: auth callout response is not for expected user")
 		}
 
-		// FIXME(dlc) - push error through here.
-		if arc.Error != nil || arc.User == nil {
-			if arc.Error != nil {
-				respCh <- fmt.Sprintf("Auth callout violation: %q on account %q", arc.Error.Description, racc.Name)
-			} else {
-				respCh <- fmt.Sprintf("Auth callout violation: no user returned  on account %q", racc.Name)
+		// check the audience to be the server ID
+		if cr.Audience != s.info.ID {
+			return nil, errors.New("Auth callout violation: auth callout response is not for server")
+		}
+
+		// check if had an error message from the auth account
+		if cr.Error != _EMPTY_ {
+			return nil, fmt.Errorf("Auth callout service returned an error: %v", cr.Error)
+		}
+
+		// if response is encrypted none of this is needed
+		if isOperatorMode && !encrypted {
+			pkStr := cr.Issuer
+			if cr.IssuerAccount != _EMPTY_ {
+				pkStr = cr.IssuerAccount
+			}
+			if pkStr != account {
+				if _, ok := acc.signingKeys[pkStr]; !ok {
+					return nil, errors.New("Auth callout signing key is unknown")
+				}
 			}
-			return
 		}
 
+		return jwt.DecodeUserClaims(cr.Jwt)
+	}
+
+	// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
+	// only in operator mode we expect to receive `issuer_account`.
+	getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
 		// Make sure correct issuer.
 		var issuer string
 		if opts.AuthCallout != nil {
@@ -116,73 +151,130 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 			// Operator mode is who we send the request on unless switching accounts.
 			issuer = acc.Name
 		}
-		// By default issuer needs to match server config or the requesting account in operator mode.
-		if arc.Issuer != issuer {
+
+		// the jwt issuer can be a signing key
+		jwtIssuer := arc.Issuer
+		if arc.IssuerAccount != _EMPTY_ {
 			if !isOperatorMode {
-				respCh <- fmt.Sprintf("Wrong issuer for auth callout response on account %q, expected %q got %q", racc.Name, issuer, arc.Issuer)
-				return
-			} else if !acc.isAllowedAcount(arc.Issuer) {
-				respCh <- fmt.Sprintf("Account %q not permitted as valid account option for auth callout for account %q",
-					arc.Issuer, racc.Name)
-				return
+				// this should be invalid - effectively it would allow the auth callout
+				// to issue on another account which may be allowed given the configuration
+				// where the auth callout account can handle multiple different ones..
+				return _EMPTY_, fmt.Errorf("Error non operator mode account %q: attempted to use issuer_account", account)
 			}
+			jwtIssuer = arc.IssuerAccount
 		}
 
-		// Require the response to have pinned the audience to this server.
-		if arc.Audience != s.info.ID {
-			respCh <- fmt.Sprintf("Wrong server audience received for auth callout response on account %q, expected %q got %q",
-				racc.Name, s.info.ID, arc.Audience)
-			return
-		}
-
-		juc := arc.User
-		// Make sure that the user is what we requested.
-		if juc.Subject != pub {
-			respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, juc.Subject, racc.Name)
-			return
+		if jwtIssuer != issuer {
+			if !isOperatorMode {
+				return _EMPTY_, fmt.Errorf("Wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
+			} else if !acc.isAllowedAcount(jwtIssuer) {
+				return _EMPTY_, fmt.Errorf("Account %q not permitted as valid account option for auth callout for account %q",
+					arc.Issuer, account)
+			}
 		}
+		return jwtIssuer, nil
+	}
 
-		allowNow, validFor := validateTimes(juc)
+	getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
+		allowNow, expiration := validateTimes(arc)
 		if !allowNow {
 			c.Errorf("Outside connect times")
-			respCh <- fmt.Sprintf("Authorized user on account %q outside of valid connect times", racc.Name)
-			return
+			return 0, nil, fmt.Errorf("Authorized user on account %q outside of valid connect times", account)
 		}
-		allowedConnTypes, err := convertAllowedConnectionTypes(juc.AllowedConnectionTypes)
+
+		allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
 		if err != nil {
 			c.Debugf("%v", err)
 			if len(allowedConnTypes) == 0 {
-				respCh <- fmt.Sprintf("Authorized user on account %q using invalid connection type", racc.Name)
-				return
+				return 0, nil, fmt.Errorf("Authorized user on account %q using invalid connection type", account)
 			}
 		}
+		return expiration, allowedConnTypes, nil
+	}
+
+	assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
 		// Apply to this client.
-		targetAcc := acc
-		// Check if we are being asked to switch accounts.
-		if aname := juc.Audience; aname != _EMPTY_ {
-			targetAcc, err = s.LookupAccount(aname)
-			if err != nil {
-				respCh <- fmt.Sprintf("No valid account %q for auth callout response on account %q: %v", aname, racc.Name, err)
-				return
-			}
-			// In operator mode make sure this account matches the issuer.
-			if isOperatorMode && aname != arc.Issuer {
-				respCh <- fmt.Sprintf("Account %q does not match issuer %q", aname, juc.Issuer)
-				return
+		var err error
+		issuerAccount, err := getIssuerAccount(arc, account)
+		if err != nil {
+			return nil, err
+		}
+
+		// if we are not in operator mode, they can specify placement as a tag
+		var placement string
+		if !isOperatorMode {
+			// only allow placement if we are not in operator mode
+			placement = arc.Audience
+		} else {
+			placement = issuerAccount
+		}
+
+		targetAcc, err := s.LookupAccount(placement)
+		if err != nil {
+			return nil, fmt.Errorf("No valid account %q for auth callout response on account %q: %v", placement, account, err)
+		}
+		if isOperatorMode {
+			// this will validate the signing key that emitted the user, and if it is a signing
+			// key it assigns the permissions from the target account
+			if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
+				return nil, fmt.Errorf("User JWT issuer %q is not known", arc.Issuer)
+			} else if scope != nil {
+				// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
+				if err := scope.ValidateScopedSigner(arc); err != nil {
+					return nil, fmt.Errorf("User JWT is not valid: %v", err)
+				} else if uSc, ok := scope.(*jwt.UserScope); !ok {
+					return nil, fmt.Errorf("User JWT is not a valid scoped user")
+				} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
+					return nil, fmt.Errorf("User JWT generated invalid permissions: %v", err)
+				}
 			}
 		}
 
+		return targetAcc, nil
+	}
+
+	processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
+		arc, err := decodeResponse(rc, rmsg, racc)
+		if err != nil {
+			respCh <- err.Error()
+			return
+		}
+		vr := jwt.CreateValidationResults()
+		arc.Validate(vr)
+		if len(vr.Issues) > 0 {
+			respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
+			return
+		}
+
+		// Make sure that the user is what we requested.
+		if arc.Subject != pub {
+			respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
+			return
+		}
+
+		expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
+		if err != nil {
+			respCh <- err.Error()
+			return
+		}
+
+		targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
+		if err != nil {
+			respCh <- err.Error()
+			return
+		}
+
 		// Build internal user and bind to the targeted account.
-		nkuser := buildInternalNkeyUser(juc, allowedConnTypes, targetAcc)
+		nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
 		if err := c.RegisterNkeyUser(nkuser); err != nil {
 			respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
 			return
 		}
 
 		// See if the response wants to override the username.
-		if juc.Name != _EMPTY_ {
+		if arc.Name != _EMPTY_ {
 			c.mu.Lock()
-			c.opts.Username = juc.Name
+			c.opts.Username = arc.Name
 			// Clear any others.
 			c.opts.Nkey = _EMPTY_
 			c.pubKey = _EMPTY_
@@ -191,11 +283,12 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 		}
 
 		// Check if we need to set an auth timer if the user jwt expires.
-		c.setExpiration(juc.Claims(), validFor)
+		c.setExpiration(arc.Claims(), expiration)
 
 		respCh <- _EMPTY_
 	}
 
+	// create a subscription to receive a response from the authcallout
 	sub, err := acc.subscribeInternal(reply, processReply)
 	if err != nil {
 		errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
@@ -209,6 +302,11 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 	if opts.AuthCallout != nil {
 		jwtSub = opts.AuthCallout.Issuer
 	}
+
+	// The public key of the server, if set is available on Varz.Key
+	// This means that when a service connects, it can now peer
+	// authenticate if it wants to - but that also means that it needs to be
+	// listening to cluster changes
 	claim := jwt.NewAuthorizationRequestClaims(jwtSub)
 	claim.Audience = AuthRequestSubject
 	// Set expected public user nkey.
@@ -228,6 +326,8 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 	claim.Server.Tags = s.getOpts().Tags
 
 	// Check if we have been requested to encrypt.
+	// FIXME: possibly this public key also needs to be on the
+	//  Varz, because then it can be peer verified?
 	if xkp != nil {
 		claim.Server.XKey = xkey
 	}
@@ -298,8 +398,11 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
 	}
 
 	// Send out our request.
-	s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false)
-
+	if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
+		errStr = fmt.Sprintf("Error sending authorization request: %v", err)
+		s.Debugf(errStr)
+		return false, errStr
+	}
 	select {
 	case errStr = <-respCh:
 		if authorized = errStr == _EMPTY_; !authorized {


Test patch:
diff --git a/server/auth_callout_test.go b/server/auth_callout_test.go
index c8a1e55d..d715d2c9 100644
--- a/server/auth_callout_test.go
+++ b/server/auth_callout_test.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
+	"sort"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -46,8 +47,33 @@ const (
 	authCalloutIssuerSeed = "SAANDLKMXL6CUS3CP52WIXBEDN6YJ545GDKC65U5JZPPV6WH6ESWUA6YAI"
 )
 
+func serviceResponse(t *testing.T, userID string, serverID string, uJwt string, errMsg string, expires time.Duration) []byte {
+	cr := jwt.NewAuthorizationResponseClaims(userID)
+	cr.Audience = serverID
+	cr.Error = errMsg
+	cr.Jwt = uJwt
+	if expires != 0 {
+		cr.Expires = time.Now().Add(expires).Unix()
+	}
+	aa, err := nkeys.FromSeed([]byte(authCalloutIssuerSeed))
+	require_NoError(t, err)
+	token, err := cr.Encode(aa)
+	require_NoError(t, err)
+	return []byte(token)
+}
+
+func makeScopedRole(t *testing.T, role string, pub []string, sub []string) (jwt.Scope, nkeys.KeyPair) {
+	akp, pk := createKey(t)
+	r := jwt.NewUserScope()
+	r.Key = pk
+	r.Template.Sub.Allow.Add(sub...)
+	r.Template.Pub.Allow.Add(pub...)
+	r.Role = role
+	return r, akp
+}
+
 // Will create a signed user jwt as an authorized user.
-func createAuthUser(t *testing.T, server, user, name, account string, akp nkeys.KeyPair, expires time.Duration, limits *jwt.UserPermissionLimits) string {
+func createAuthUser(t *testing.T, user, name, account, issuerAccount string, akp nkeys.KeyPair, expires time.Duration, limits *jwt.UserPermissionLimits) string {
 	t.Helper()
 
 	if akp == nil {
@@ -56,38 +82,93 @@ func createAuthUser(t *testing.T, server, user, name, account string, akp nkeys.
 		require_NoError(t, err)
 	}
 
-	uclaim := jwt.NewUserClaims(user)
-	uclaim.Audience = account
+	uc := jwt.NewUserClaims(user)
+	if issuerAccount != "" {
+		if _, err := nkeys.FromPublicKey(issuerAccount); err != nil {
+			t.Fatalf("issuer account is not a public key: %v", err)
+		}
+		uc.IssuerAccount = issuerAccount
+	}
+	// The callout uses the audience as the target account
+	// only if in non-operator mode, otherwise the user JWT has
+	// correct attribution - issuer or issuer_account
+	if _, err := nkeys.FromPublicKey(account); err != nil {
+		// if it is not a public key, set the audience
+		uc.Audience = account
+	}
+
 	if name != _EMPTY_ {
-		uclaim.Name = name
+		uc.Name = name
 	}
-	if expires > 0 {
-		uclaim.Expires = time.Now().Add(expires).Unix()
+	if expires != 0 {
+		uc.Expires = time.Now().Add(expires).Unix()
 	}
 	if limits != nil {
-		uclaim.UserPermissionLimits = *limits
+		uc.UserPermissionLimits = *limits
 	}
 
 	vr := jwt.CreateValidationResults()
-	uclaim.Validate(vr)
+	uc.Validate(vr)
 	require_Len(t, len(vr.Errors()), 0)
 
-	arc := jwt.NewAuthorizationResponseClaims(user)
-	arc.User = uclaim
-	arc.Audience = server
+	tok, err := uc.Encode(akp)
+	require_NoError(t, err)
 
-	vr = jwt.CreateValidationResults()
-	arc.Validate(vr)
-	require_Len(t, len(vr.Errors()), 0)
+	return tok
+}
+
+type authTest struct {
+	t          *testing.T
+	srv        *Server
+	conf       string
+	authClient *nats.Conn
+	clients    []*nats.Conn
+}
+
+func NewAuthTest(t *testing.T, config string, authHandler nats.MsgHandler, clientOptions ...nats.Option) *authTest {
+	a := &authTest{t: t}
+	a.conf = createConfFile(t, []byte(config))
+	a.srv, _ = RunServerWithConfig(a.conf)
 
-	rjwt, err := arc.Encode(akp)
+	var err error
+	a.authClient = a.Connect(clientOptions...)
+	_, err = a.authClient.Subscribe(AuthCalloutSubject, authHandler)
 	require_NoError(t, err)
+	return a
+}
+
+func (at *authTest) NewClient(clientOptions ...nats.Option) (*nats.Conn, error) {
+	conn, err := nats.Connect(at.srv.ClientURL(), clientOptions...)
+	if err != nil {
+		return nil, err
+	}
+	at.clients = append(at.clients, conn)
+	return conn, nil
+}
+
+func (at *authTest) Connect(clientOptions ...nats.Option) *nats.Conn {
+	conn, err := at.NewClient(clientOptions...)
+	require_NoError(at.t, err)
+	return conn
+}
 
-	return rjwt
+func (at *authTest) RequireConnectError(clientOptions ...nats.Option) {
+	_, err := at.NewClient(clientOptions...)
+	require_Error(at.t, err)
+}
+
+func (at *authTest) Cleanup() {
+	if at.authClient != nil {
+		at.authClient.Close()
+	}
+	if at.srv != nil {
+		at.srv.Shutdown()
+		removeFile(at.t, at.conf)
+	}
 }
 
 func TestAuthCalloutBasics(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		authorization {
@@ -100,22 +181,9 @@ func TestAuthCalloutBasics(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
+	`
 	callouts := uint32(0)
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
-	// Make sure callout was not triggered.
-	require_True(t, atomic.LoadUint32(&callouts) == 0)
-
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		atomic.AddUint32(&callouts, 1)
 		user, si, ci, opts, _ := decodeAuthRequest(t, m.Data)
 		require_True(t, si.Name == "A")
@@ -125,23 +193,21 @@ func TestAuthCalloutBasics(t *testing.T) {
 			var j jwt.UserPermissionLimits
 			j.Pub.Allow.Add("$SYS.>")
 			j.Payload = 1024
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 10*time.Minute, &j)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 10*time.Minute, &j)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else {
 			// Nil response signals no authentication.
 			m.Respond(nil)
 		}
-	})
-	require_NoError(t, err)
+	}
+	at := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer at.Cleanup()
 
 	// This one should fail since bad password.
-	_, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "xxx"))
-	require_Error(t, err)
+	at.RequireConnectError(nats.UserInfo("dlc", "xxx"))
 
 	// This one will use callout since not defined in server config.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "zzz"))
-	require_NoError(t, err)
-	defer nc.Close()
+	nc := at.Connect(nats.UserInfo("dlc", "zzz"))
 
 	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
 	require_NoError(t, err)
@@ -173,7 +239,7 @@ func TestAuthCalloutBasics(t *testing.T) {
 }
 
 func TestAuthCalloutMultiAccounts(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: ZZ
 		accounts {
@@ -191,35 +257,26 @@ func TestAuthCalloutMultiAccounts(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// Auth callout user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-	// Should always make auth callouts queue subscribers or proper services.
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	`
+	handler := func(m *nats.Msg) {
 		user, si, ci, opts, _ := decodeAuthRequest(t, m.Data)
 		require_True(t, si.Name == "ZZ")
 		require_True(t, ci.Host == "127.0.0.1")
 		// Allow dlc user and map to the BAZ account.
 		if opts.Username == "dlc" && opts.Password == "zzz" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, "BAZ", nil, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, "BAZ", "", nil, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else {
 			// Nil response signals no authentication.
 			m.Respond(nil)
 		}
-	})
-	require_NoError(t, err)
+	}
+
+	at := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer at.Cleanup()
 
 	// This one will use callout since not defined in server config.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "zzz"))
-	require_NoError(t, err)
-	defer nc.Close()
+	nc := at.Connect(nats.UserInfo("dlc", "zzz"))
 
 	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
 	require_NoError(t, err)
@@ -233,7 +290,7 @@ func TestAuthCalloutMultiAccounts(t *testing.T) {
 }
 
 func TestAuthCalloutClientTLSCerts(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "localhost:-1"
 		server_name: T
 
@@ -257,20 +314,8 @@ func TestAuthCalloutClientTLSCerts(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	nc, err := nats.Connect(s.ClientURL(),
-		nats.UserInfo("auth", "pwd"),
-		nats.ClientCert("../test/configs/certs/tlsauth/client2.pem", "../test/configs/certs/tlsauth/client2-key.pem"),
-		nats.RootCAs("../test/configs/certs/tlsauth/ca.pem"),
-	)
-	require_NoError(t, err)
-	defer nc.Close()
-
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	`
+	handler := func(m *nats.Msg) {
 		user, si, ci, _, ctls := decodeAuthRequest(t, m.Data)
 		require_True(t, si.Name == "T")
 		require_True(t, ci.Host == "127.0.0.1")
@@ -285,19 +330,22 @@ func TestAuthCalloutClientTLSCerts(t *testing.T) {
 		require_NoError(t, err)
 		if strings.HasPrefix(cert.Subject.String(), "CN=example.com") {
 			// Override blank name here, server will substitute.
-			ujwt := createAuthUser(t, si.ID, user, "dlc", "FOO", nil, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, "dlc", "FOO", "", nil, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		}
-	})
-	require_NoError(t, err)
+	}
+
+	ac := NewAuthTest(t, conf, handler,
+		nats.UserInfo("auth", "pwd"),
+		nats.ClientCert("../test/configs/certs/tlsauth/client2.pem", "../test/configs/certs/tlsauth/client2-key.pem"),
+		nats.RootCAs("../test/configs/certs/tlsauth/ca.pem"))
+	defer ac.Cleanup()
 
 	// Will use client cert to determine user.
-	nc, err = nats.Connect(s.ClientURL(),
+	nc := ac.Connect(
 		nats.ClientCert("../test/configs/certs/tlsauth/client2.pem", "../test/configs/certs/tlsauth/client2-key.pem"),
 		nats.RootCAs("../test/configs/certs/tlsauth/ca.pem"),
 	)
-	require_NoError(t, err)
-	defer nc.Close()
 
 	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
 	require_NoError(t, err)
@@ -311,7 +359,7 @@ func TestAuthCalloutClientTLSCerts(t *testing.T) {
 }
 
 func TestAuthCalloutVerifiedUserCalloutsWithSig(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		authorization {
@@ -327,40 +375,28 @@ func TestAuthCalloutVerifiedUserCalloutsWithSig(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
+	`
 	callouts := uint32(0)
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		atomic.AddUint32(&callouts, 1)
 		user, si, ci, opts, _ := decodeAuthRequest(t, m.Data)
 		require_True(t, si.Name == "A")
 		require_True(t, ci.Host == "127.0.0.1")
 		require_True(t, opts.SignedNonce != _EMPTY_)
 		require_True(t, ci.Nonce != _EMPTY_)
-		ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 0, nil)
-		m.Respond([]byte(ujwt))
-	})
-	require_NoError(t, err)
+		ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 0, nil)
+		m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+	}
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
 
-	// This one will use callout since not part of auth_users.
-	// Even though we will internally verify this user the callout will still be called.
 	seedFile := createTempFile(t, _EMPTY_)
 	defer removeFile(t, seedFile.Name())
 	seedFile.WriteString(authCalloutSeed)
 	nkeyOpt, err := nats.NkeyOptionFromSeed(seedFile.Name())
 	require_NoError(t, err)
 
-	nc, err = nats.Connect(s.ClientURL(), nkeyOpt)
-	require_NoError(t, err)
-	defer nc.Close()
+	nc := ac.Connect(nkeyOpt)
 
 	// Make sure that the callout was called.
 	if atomic.LoadUint32(&callouts) != 1 {
@@ -452,8 +488,12 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 
 	// TEST account.
 	tkp, tpub := createKey(t)
+	tSigningKp, tSigningPub := createKey(t)
 	accClaim := jwt.NewAccountClaims(tpub)
 	accClaim.Name = "TEST"
+	accClaim.SigningKeys.Add(tSigningPub)
+	scope, scopedKp := makeScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"})
+	accClaim.SigningKeys.AddScopedSigner(scope)
 	accJwt, err := accClaim.Encode(oKp)
 	require_NoError(t, err)
 
@@ -475,7 +515,7 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 	authJwt, err := authClaim.Encode(oKp)
 	require_NoError(t, err)
 
-	conf := createConfFile(t, []byte(fmt.Sprintf(`
+	conf := fmt.Sprintf(`
 		listen: 127.0.0.1:-1
 		operator: %s
 		system_account: %s
@@ -485,18 +525,40 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 			%s: %s
 			%s: %s
 		}
-    `, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)))
-	defer removeFile(t, conf)
-
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
+    `, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)
 
-	nc, err := nats.Connect(s.ClientURL(), nats.UserCredentials(creds))
-	require_NoError(t, err)
-	defer nc.Close()
+	const secretToken = "--XX--"
+	const dummyToken = "--ZZ--"
+	const skKeyToken = "--SK--"
+	const scopedToken = "--Scoped--"
+	const badScopedToken = "--BADScoped--"
+	dkp, notAllowAccountPub := createKey(t)
+	handler := func(m *nats.Msg) {
+		user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
+		if opts.Token == secretToken {
+			ujwt := createAuthUser(t, user, "dlc", tpub, "", tkp, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+		} else if opts.Token == dummyToken {
+			ujwt := createAuthUser(t, user, "dummy", notAllowAccountPub, "", dkp, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+		} else if opts.Token == skKeyToken {
+			ujwt := createAuthUser(t, user, "sk", tpub, tpub, tSigningKp, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+		} else if opts.Token == scopedToken {
+			// must have no limits set
+			ujwt := createAuthUser(t, user, "scoped", tpub, tpub, scopedKp, 0, &jwt.UserPermissionLimits{})
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+		} else if opts.Token == badScopedToken {
+			// limits are nil - here which result in a default user - this will fail scoped
+			ujwt := createAuthUser(t, user, "bad-scoped", tpub, tpub, scopedKp, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+		} else {
+			m.Respond(nil)
+		}
+	}
 
-	// Check that we have the deny permission autoset properly.
-	resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
+	ac := NewAuthTest(t, conf, handler, nats.UserCredentials(creds))
+	resp, err := ac.authClient.Request(userDirectInfoSubj, nil, time.Second)
 	require_NoError(t, err)
 	response := ServerAPIResponse{Data: &UserInfo{}}
 	err = json.Unmarshal(resp.Data, &response)
@@ -517,25 +579,6 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 		t.Fatalf("User info did not match expected, expected auto-deny permissions on callout subject")
 	}
 
-	const secretToken = "--XX--"
-	const dummyToken = "--ZZ--"
-	dkp, notAllowAccountPub := createKey(t)
-
-	// Register authorization handlers.
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
-		user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
-		if opts.Token == secretToken {
-			ujwt := createAuthUser(t, si.ID, user, "dlc", tpub, tkp, 0, nil)
-			m.Respond([]byte(ujwt))
-		} else if opts.Token == dummyToken {
-			ujwt := createAuthUser(t, si.ID, user, "dummy", notAllowAccountPub, dkp, 0, nil)
-			m.Respond([]byte(ujwt))
-		} else {
-			m.Respond(nil)
-		}
-	})
-	require_NoError(t, err)
-
 	// Bearer token etc..
 	// This is used by all users, and the customization will be in other connect args.
 	// This needs to also be bound to the authorization account.
@@ -543,13 +586,11 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 	defer removeFile(t, creds)
 
 	// We require a token.
-	_, err = nats.Connect(s.ClientURL(), nats.UserCredentials(creds))
-	require_Error(t, err)
+	ac.RequireConnectError(nats.UserCredentials(creds))
 
 	// Send correct token. This should switch us to the test account.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserCredentials(creds), nats.Token(secretToken))
+	nc := ac.Connect(nats.UserCredentials(creds), nats.Token(secretToken))
 	require_NoError(t, err)
-	defer nc.Close()
 
 	resp, err = nc.Request(userDirectInfoSubj, nil, time.Second)
 	require_NoError(t, err)
@@ -565,8 +606,47 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
 	}
 
 	// Now make sure that if the authorization service switches to an account that is not allowed, we reject.
-	_, err = nats.Connect(s.ClientURL(), nats.UserCredentials(creds), nats.Token(dummyToken))
-	require_Error(t, err)
+	ac.RequireConnectError(nats.UserCredentials(creds), nats.Token(dummyToken))
+
+	// Send the signing key token. This should switch us to the test account, but the user
+	// is signed with the account signing key
+	nc = ac.Connect(nats.UserCredentials(creds), nats.Token(skKeyToken))
+
+	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)
+	if userInfo.Account != tpub {
+		t.Fatalf("Expected to be switched to %q, but got %q", tpub, userInfo.Account)
+	}
+
+	// bad scoped user
+	ac.RequireConnectError(nats.UserCredentials(creds), nats.Token(badScopedToken))
+
+	// Send the signing key token. This should switch us to the test account, but the user
+	// is signed with the account signing key
+	nc = ac.Connect(nats.UserCredentials(creds), nats.Token(scopedToken))
+	require_NoError(t, err)
+
+	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)
+	if userInfo.Account != tpub {
+		t.Fatalf("Expected to be switched to %q, but got %q", tpub, userInfo.Account)
+	}
+	require_True(t, len(userInfo.Permissions.Publish.Allow) == 2)
+	sort.Strings(userInfo.Permissions.Publish.Allow)
+	require_Equal(t, "foo.>", userInfo.Permissions.Publish.Allow[1])
+	sort.Strings(userInfo.Permissions.Subscribe.Allow)
+	require_True(t, len(userInfo.Permissions.Subscribe.Allow) == 2)
+	require_Equal(t, "foo.>", userInfo.Permissions.Subscribe.Allow[1])
 }
 
 const (
@@ -591,20 +671,12 @@ func TestAuthCalloutServerConfigEncryption(t *testing.T) {
 			}
 		}
 	`
-	conf := createConfFile(t, []byte(fmt.Sprintf(tmpl, curvePublic)))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
+	conf := fmt.Sprintf(tmpl, curvePublic)
 
 	rkp, err := nkeys.FromCurveSeed([]byte(curveSeed))
 	require_NoError(t, err)
 
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		// This will be encrypted.
 		_, err := jwt.DecodeAuthorizationRequestClaims(string(m.Data))
 		require_Error(t, err)
@@ -620,29 +692,27 @@ func TestAuthCalloutServerConfigEncryption(t *testing.T) {
 		require_True(t, ci.Host == "127.0.0.1")
 		// Allow dlc user.
 		if opts.Username == "dlc" && opts.Password == "zzz" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 10*time.Minute, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 10*time.Minute, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else if opts.Username == "dlc" && opts.Password == "xxx" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 10*time.Minute, nil)
+			ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 10*time.Minute, nil)
 			// Encrypt this response.
-			encryptedResponse, err := rkp.Seal([]byte(ujwt), si.XKey) // Server's public xkey.
+			data, err := rkp.Seal(serviceResponse(t, user, si.ID, ujwt, "", 0), si.XKey) // Server's public xkey.
 			require_NoError(t, err)
-			m.Respond(encryptedResponse)
+			m.Respond(data)
 		} else {
 			// Nil response signals no authentication.
 			m.Respond(nil)
 		}
-	})
-	require_NoError(t, err)
+	}
 
-	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "zzz"))
-	require_NoError(t, err)
-	defer nc.Close()
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
 
-	// Authorization services can optionally encrypt the reponses using the server's public xkey.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "xxx"))
-	require_NoError(t, err)
-	defer nc.Close()
+	ac.Connect(nats.UserInfo("dlc", "zzz"))
+
+	// Authorization services can optionally encrypt the responses using the server's public xkey.
+	ac.Connect(nats.UserInfo("dlc", "xxx"))
 }
 
 func TestAuthCalloutOperatorModeEncryption(t *testing.T) {
@@ -679,7 +749,7 @@ func TestAuthCalloutOperatorModeEncryption(t *testing.T) {
 	authJwt, err := authClaim.Encode(oKp)
 	require_NoError(t, err)
 
-	conf := createConfFile(t, []byte(fmt.Sprintf(`
+	conf := fmt.Sprintf(`
 		listen: 127.0.0.1:-1
 		operator: %s
 		system_account: %s
@@ -689,24 +759,15 @@ func TestAuthCalloutOperatorModeEncryption(t *testing.T) {
 			%s: %s
 			%s: %s
 		}
-    `, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)))
-	defer removeFile(t, conf)
-
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
+    `, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)
 
-	nc, err := nats.Connect(s.ClientURL(), nats.UserCredentials(creds))
+	rkp, err := nkeys.FromCurveSeed([]byte(curveSeed))
 	require_NoError(t, err)
-	defer nc.Close()
 
 	const tokenA = "--XX--"
 	const tokenB = "--ZZ--"
 
-	rkp, err := nkeys.FromCurveSeed([]byte(curveSeed))
-	require_NoError(t, err)
-
-	// Register authorization handlers.
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		// Make sure this is an encrypted request.
 		if bytes.HasPrefix(m.Data, []byte(jwtPrefix)) {
 			t.Fatalf("Request not encrypted")
@@ -720,19 +781,20 @@ func TestAuthCalloutOperatorModeEncryption(t *testing.T) {
 		require_True(t, si.XKey == xkey)
 		require_True(t, ci.Host == "127.0.0.1")
 		if opts.Token == tokenA {
-			ujwt := createAuthUser(t, si.ID, user, "dlc", tpub, tkp, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, "dlc", tpub, "", tkp, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else if opts.Token == tokenB {
-			ujwt := createAuthUser(t, si.ID, user, "rip", tpub, tkp, 0, nil)
+			ujwt := createAuthUser(t, user, "rip", tpub, "", tkp, 0, nil)
 			// Encrypt this response.
-			encryptedResponse, err := rkp.Seal([]byte(ujwt), si.XKey) // Server's public xkey.
+			data, err := rkp.Seal(serviceResponse(t, user, si.ID, ujwt, "", 0), si.XKey) // Server's public xkey.
 			require_NoError(t, err)
-			m.Respond(encryptedResponse)
+			m.Respond(data)
 		} else {
 			m.Respond(nil)
 		}
-	})
-	require_NoError(t, err)
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserCredentials(creds))
 
 	// Bearer token etc..
 	// This is used by all users, and the customization will be in other connect args.
@@ -741,18 +803,14 @@ func TestAuthCalloutOperatorModeEncryption(t *testing.T) {
 	defer removeFile(t, creds)
 
 	// This will receive an encrypted request to the auth service but send plaintext response.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserCredentials(creds), nats.Token(tokenA))
-	require_NoError(t, err)
-	defer nc.Close()
+	ac.Connect(nats.UserCredentials(creds), nats.Token(tokenA))
 
 	// This will receive an encrypted request to the auth service and send an encrypted response.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserCredentials(creds), nats.Token(tokenB))
-	require_NoError(t, err)
-	defer nc.Close()
+	ac.Connect(nats.UserCredentials(creds), nats.Token(tokenB))
 }
 
 func TestAuthCalloutServerTags(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		server_tags: ["foo", "bar"]
@@ -763,27 +821,20 @@ func TestAuthCalloutServerTags(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
+	`
 
 	tch := make(chan jwt.TagList, 1)
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
 		tch <- si.Tags
-		ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 10*time.Minute, nil)
-		m.Respond([]byte(ujwt))
-	})
-	require_NoError(t, err)
+		ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 10*time.Minute, nil)
+		m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+	}
 
-	_, err = nats.Connect(s.ClientURL())
-	require_NoError(t, err)
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	ac.Connect()
 
 	tags := <-tch
 	require_True(t, len(tags) == 2)
@@ -792,7 +843,7 @@ func TestAuthCalloutServerTags(t *testing.T) {
 }
 
 func TestAuthCalloutServerClusterAndVersion(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		authorization {
@@ -803,28 +854,20 @@ func TestAuthCalloutServerClusterAndVersion(t *testing.T) {
 			}
 		}
 		cluster { name: HUB }
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
+	`
 	ch := make(chan string, 2)
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
 		ch <- si.Cluster
 		ch <- si.Version
-		ujwt := createAuthUser(t, si.ID, user, _EMPTY_, globalAccountName, nil, 10*time.Minute, nil)
-		m.Respond([]byte(ujwt))
-	})
-	require_NoError(t, err)
+		ujwt := createAuthUser(t, user, _EMPTY_, globalAccountName, "", nil, 10*time.Minute, nil)
+		m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+	}
 
-	_, err = nats.Connect(s.ClientURL())
-	require_NoError(t, err)
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	ac.Connect()
 
 	cluster := <-ch
 	require_True(t, cluster == "HUB")
@@ -836,30 +879,8 @@ func TestAuthCalloutServerClusterAndVersion(t *testing.T) {
 	require_True(t, ok)
 }
 
-func createErrResponse(t *testing.T, user, server, errStr string, akp nkeys.KeyPair) string {
-	t.Helper()
-
-	if akp == nil {
-		var err error
-		akp, err = nkeys.FromSeed([]byte(authCalloutIssuerSeed))
-		require_NoError(t, err)
-	}
-
-	arc := jwt.NewAuthorizationResponseClaims(user)
-	arc.SetErrorDescription(errStr)
-	arc.Audience = server
-	vr := jwt.CreateValidationResults()
-	arc.Validate(vr)
-	require_Len(t, len(vr.Errors()), 0)
-
-	rjwt, err := arc.Encode(akp)
-	require_NoError(t, err)
-
-	return rjwt
-}
-
 func TestAuthCalloutErrorResponse(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		authorization {
@@ -869,29 +890,20 @@ func TestAuthCalloutErrorResponse(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	// This will not use callout since predefined as an auth_user.
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	`
+	handler := func(m *nats.Msg) {
 		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
-		errResp := createErrResponse(t, user, si.ID, "BAD AUTH", nil)
-		m.Respond([]byte(errResp))
-	})
-	require_NoError(t, err)
+		m.Respond(serviceResponse(t, user, si.ID, "", "BAD AUTH", 0))
+	}
 
-	_, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "zzz"))
-	require_Error(t, err)
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	ac.RequireConnectError(nats.UserInfo("dlc", "zzz"))
 }
 
 func TestAuthCalloutAuthUserFailDoesNotInvokeCallout(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		authorization {
@@ -901,26 +913,18 @@ func TestAuthCalloutAuthUserFailDoesNotInvokeCallout(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
+	`
 	callouts := uint32(0)
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
-		atomic.AddUint32(&callouts, 1)
+	handler := func(m *nats.Msg) {
 		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
-		errResp := createErrResponse(t, user, si.ID, "BAD AUTH", nil)
-		m.Respond([]byte(errResp))
-	})
-	require_NoError(t, err)
+		atomic.AddUint32(&callouts, 1)
+		m.Respond(serviceResponse(t, user, si.ID, "", "WRONG PASSWORD", 0))
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
 
-	_, err = nats.Connect(s.ClientURL(), nats.UserInfo("auth", "zzz"))
-	require_Error(t, err)
+	ac.RequireConnectError(nats.UserInfo("auth", "zzz"))
 
 	if atomic.LoadUint32(&callouts) != 0 {
 		t.Fatalf("Expected callout to not be called")
@@ -928,7 +932,7 @@ func TestAuthCalloutAuthUserFailDoesNotInvokeCallout(t *testing.T) {
 }
 
 func TestAuthCalloutAuthErrEvents(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		accounts {
@@ -943,44 +947,34 @@ func TestAuthCalloutAuthErrEvents(t *testing.T) {
 				auth_users: [ auth ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
-
-	// This is where the event fires, in this account.
-	sub, err := nc.SubscribeSync(authErrorAccountEventSubj)
-	require_NoError(t, err)
+	`
 
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
 		// Allow dlc user and map to the BAZ account.
 		if opts.Username == "dlc" && opts.Password == "zzz" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, "FOO", nil, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, "FOO", "", nil, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else if opts.Username == "dlc" {
-			errResp := createErrResponse(t, user, si.ID, "WRONG PASSWORD", nil)
-			m.Respond([]byte(errResp))
+			m.Respond(serviceResponse(t, user, si.ID, "", "WRONG PASSWORD", 0))
 		} else {
-			errResp := createErrResponse(t, user, si.ID, "BAD CREDS", nil)
-			m.Respond([]byte(errResp))
+			m.Respond(serviceResponse(t, user, si.ID, "", "BAD CREDS", 0))
 		}
-	})
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	// This is where the event fires, in this account.
+	sub, err := ac.authClient.SubscribeSync(authErrorAccountEventSubj)
 	require_NoError(t, err)
 
 	// This one will use callout since not defined in server config.
-	nc, err = nats.Connect(s.ClientURL(), nats.UserInfo("dlc", "zzz"))
-	require_NoError(t, err)
-	nc.Close()
+	ac.Connect(nats.UserInfo("dlc", "zzz"))
 	checkSubsPending(t, sub, 0)
 
 	checkAuthErrEvent := func(user, pass, reason string) {
-		_, err = nats.Connect(s.ClientURL(), nats.UserInfo(user, pass))
-		require_Error(t, err)
+		ac.RequireConnectError(nats.UserInfo(user, pass))
 
 		m, err := sub.NextMsg(time.Second)
 		require_NoError(t, err)
@@ -992,7 +986,6 @@ func TestAuthCalloutAuthErrEvents(t *testing.T) {
 		if !strings.Contains(dm.Reason, reason) {
 			t.Fatalf("Expected %q reason, but got %q", reason, dm.Reason)
 		}
-
 	}
 
 	checkAuthErrEvent("dlc", "xxx", "WRONG PASSWORD")
@@ -1000,7 +993,7 @@ func TestAuthCalloutAuthErrEvents(t *testing.T) {
 }
 
 func TestAuthCalloutConnectEvents(t *testing.T) {
-	conf := createConfFile(t, []byte(`
+	conf := `
 		listen: "127.0.0.1:-1"
 		server_name: A
 		accounts {
@@ -1016,35 +1009,27 @@ func TestAuthCalloutConnectEvents(t *testing.T) {
 				auth_users: [ auth, admin ]
 			}
 		}
-	`))
-	defer removeFile(t, conf)
-	s, _ := RunServerWithConfig(conf)
-	defer s.Shutdown()
-
-	nc, err := nats.Connect(s.ClientURL(), nats.UserInfo("auth", "pwd"))
-	require_NoError(t, err)
-	defer nc.Close()
+	`
 
-	_, err = nc.Subscribe(AuthCalloutSubject, func(m *nats.Msg) {
+	handler := func(m *nats.Msg) {
 		user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
 		// Allow dlc user and map to the BAZ account.
 		if opts.Username == "dlc" && opts.Password == "zzz" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, "FOO", nil, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, "FOO", "", nil, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else if opts.Username == "rip" && opts.Password == "xxx" {
-			ujwt := createAuthUser(t, si.ID, user, _EMPTY_, "BAR", nil, 0, nil)
-			m.Respond([]byte(ujwt))
+			ujwt := createAuthUser(t, user, _EMPTY_, "BAR", "", nil, 0, nil)
+			m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
 		} else {
-			errResp := createErrResponse(t, user, si.ID, "BAD CREDS", nil)
-			m.Respond([]byte(errResp))
+			m.Respond(serviceResponse(t, user, si.ID, "", "BAD CREDS", 0))
 		}
-	})
-	require_NoError(t, err)
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
 
 	// Setup system user.
-	snc, err := nats.Connect(s.ClientURL(), nats.UserInfo("admin", "s3cr3t!"))
-	require_NoError(t, err)
-	defer nc.Close()
+	snc := ac.Connect(nats.UserInfo("admin", "s3cr3t!"))
 
 	// Allow this connect event to pass us by..
 	time.Sleep(250 * time.Millisecond)
@@ -1068,7 +1053,7 @@ func TestAuthCalloutConnectEvents(t *testing.T) {
 	snc.Flush()
 
 	checkConnectEvents := func(user, pass, acc string) {
-		nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, pass))
+		nc := ac.Connect(nats.UserInfo(user, pass))
 		require_NoError(t, err)
 
 		m, err := csub.NextMsg(time.Second)
@@ -1133,3 +1118,199 @@ func TestAuthCalloutConnectEvents(t *testing.T) {
 	checkConnectEvents("dlc", "zzz", "FOO")
 	checkConnectEvents("rip", "xxx", "BAR")
 }
+
+func TestAuthCalloutBadServer(t *testing.T) {
+	conf := `
+		listen: "127.0.0.1:-1"
+		server_name: A
+		accounts {
+            AUTH { users [ {user: "auth", password: "pwd"} ] }
+			FOO {}
+		}
+		authorization {
+			auth_callout {
+				issuer: "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
+				account: AUTH
+				auth_users: [ auth ]
+			}
+		}
+	`
+
+	handler := func(m *nats.Msg) {
+		user, _, _, _, _ := decodeAuthRequest(t, m.Data)
+		skp, err := nkeys.CreateServer()
+		require_NoError(t, err)
+		spk, err := skp.PublicKey()
+		require_NoError(t, err)
+		ujwt := createAuthUser(t, user, _EMPTY_, "FOO", "", nil, 0, nil)
+		m.Respond(serviceResponse(t, user, spk, ujwt, "", 0))
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	// This is where the event fires, in this account.
+	sub, err := ac.authClient.SubscribeSync(authErrorAccountEventSubj)
+	require_NoError(t, err)
+
+	checkAuthErrEvent := func(user, pass, reason string) {
+		ac.RequireConnectError(nats.UserInfo(user, pass))
+
+		m, err := sub.NextMsg(time.Second)
+		require_NoError(t, err)
+
+		var dm DisconnectEventMsg
+		err = json.Unmarshal(m.Data, &dm)
+		require_NoError(t, err)
+
+		if !strings.Contains(dm.Reason, reason) {
+			t.Fatalf("Expected %q reason, but got %q", reason, dm.Reason)
+		}
+	}
+	checkAuthErrEvent("hello", "world", "response is not for server")
+}
+
+func TestAuthCalloutBadUser(t *testing.T) {
+	conf := `
+		listen: "127.0.0.1:-1"
+		server_name: A
+		accounts {
+            AUTH { users [ {user: "auth", password: "pwd"} ] }
+			FOO {}
+		}
+		authorization {
+			auth_callout {
+				issuer: "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
+				account: AUTH
+				auth_users: [ auth ]
+			}
+		}
+	`
+
+	handler := func(m *nats.Msg) {
+		_, si, _, _, _ := decodeAuthRequest(t, m.Data)
+		kp, err := nkeys.CreateUser()
+		require_NoError(t, err)
+		upk, err := kp.PublicKey()
+		require_NoError(t, err)
+		ujwt := createAuthUser(t, upk, _EMPTY_, "FOO", "", nil, 0, nil)
+		m.Respond(serviceResponse(t, upk, si.ID, ujwt, "", 0))
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	// This is where the event fires, in this account.
+	sub, err := ac.authClient.SubscribeSync(authErrorAccountEventSubj)
+	require_NoError(t, err)
+
+	checkAuthErrEvent := func(user, pass, reason string) {
+		ac.RequireConnectError(nats.UserInfo(user, pass))
+
+		m, err := sub.NextMsg(time.Second)
+		require_NoError(t, err)
+
+		var dm DisconnectEventMsg
+		err = json.Unmarshal(m.Data, &dm)
+		require_NoError(t, err)
+
+		if !strings.Contains(dm.Reason, reason) {
+			t.Fatalf("Expected %q reason, but got %q", reason, dm.Reason)
+		}
+	}
+	checkAuthErrEvent("hello", "world", "auth callout response is not for expected user")
+}
+
+func TestAuthCalloutExpiredUser(t *testing.T) {
+	conf := `
+		listen: "127.0.0.1:-1"
+		server_name: A
+		accounts {
+            AUTH { users [ {user: "auth", password: "pwd"} ] }
+			FOO {}
+		}
+		authorization {
+			auth_callout {
+				issuer: "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
+				account: AUTH
+				auth_users: [ auth ]
+			}
+		}
+	`
+
+	handler := func(m *nats.Msg) {
+		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
+		ujwt := createAuthUser(t, user, _EMPTY_, "FOO", "", nil, time.Second*-5, nil)
+		m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	// This is where the event fires, in this account.
+	sub, err := ac.authClient.SubscribeSync(authErrorAccountEventSubj)
+	require_NoError(t, err)
+
+	checkAuthErrEvent := func(user, pass, reason string) {
+		ac.RequireConnectError(nats.UserInfo(user, pass))
+
+		m, err := sub.NextMsg(time.Second)
+		require_NoError(t, err)
+
+		var dm DisconnectEventMsg
+		err = json.Unmarshal(m.Data, &dm)
+		require_NoError(t, err)
+
+		if !strings.Contains(dm.Reason, reason) {
+			t.Fatalf("Expected %q reason, but got %q", reason, dm.Reason)
+		}
+	}
+	checkAuthErrEvent("hello", "world", "claim is expired")
+}
+
+func TestAuthCalloutExpiredResponse(t *testing.T) {
+	conf := `
+		listen: "127.0.0.1:-1"
+		server_name: A
+		accounts {
+            AUTH { users [ {user: "auth", password: "pwd"} ] }
+			FOO {}
+		}
+		authorization {
+			auth_callout {
+				issuer: "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
+				account: AUTH
+				auth_users: [ auth ]
+			}
+		}
+	`
+
+	handler := func(m *nats.Msg) {
+		user, si, _, _, _ := decodeAuthRequest(t, m.Data)
+		ujwt := createAuthUser(t, user, _EMPTY_, "FOO", "", nil, 0, nil)
+		m.Respond(serviceResponse(t, user, si.ID, ujwt, "", time.Second*-5))
+	}
+
+	ac := NewAuthTest(t, conf, handler, nats.UserInfo("auth", "pwd"))
+	defer ac.Cleanup()
+
+	// This is where the event fires, in this account.
+	sub, err := ac.authClient.SubscribeSync(authErrorAccountEventSubj)
+	require_NoError(t, err)
+
+	checkAuthErrEvent := func(user, pass, reason string) {
+		ac.RequireConnectError(nats.UserInfo(user, pass))
+
+		m, err := sub.NextMsg(time.Second)
+		require_NoError(t, err)
+
+		var dm DisconnectEventMsg
+		err = json.Unmarshal(m.Data, &dm)
+		require_NoError(t, err)
+
+		if !strings.Contains(dm.Reason, reason) {
+			t.Fatalf("Expected %q reason, but got %q", reason, dm.Reason)
+		}
+	}
+	checkAuthErrEvent("hello", "world", "claim is expired")
+}