Skip to content

Commit

Permalink
[FIX] authcallout - cleared client jwt options as onboarding the clie…
Browse files Browse the repository at this point in the history
…nt would reparse the token (which was the bearer token to connect to the callout for limits such as subs/data/payload (#5013)

FIX #5002
  • Loading branch information
derekcollison committed Jan 30, 2024
2 parents d413e33 + 16a8ba7 commit 3bb4807
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 4 deletions.
9 changes: 8 additions & 1 deletion server/auth_callout.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
}
}
}

return targetAcc, nil
}

Expand Down Expand Up @@ -270,6 +269,14 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
return
}

// the JWT is cleared, because if in operator mode it may hold the JWT
// for the bearer token that connected to the callout if in operator mode
// the permissions are already set on the client, this prevents a decode
// on c.RegisterNKeyUser which would have wrong values
c.mu.Lock()
c.opts.JWT = _EMPTY_
c.mu.Unlock()

// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
Expand Down
193 changes: 190 additions & 3 deletions server/auth_callout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
authCalloutSeed = "SUAP277QP7U4JMFFPVZHLJYEQJ2UHOTYVEIZJYAWRJXQLP4FRSEHYZJJOU"
authCalloutIssuer = "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
authCalloutIssuerSeed = "SAANDLKMXL6CUS3CP52WIXBEDN6YJ545GDKC65U5JZPPV6WH6ESWUA6YAI"
authCalloutIssuerSK = "SAAE46BB675HKZKSVJEUZAKKWIV6BJJO6XYE46Z3ZHO7TCI647M3V42IJE"
)

func serviceResponse(t *testing.T, userID string, serverID string, uJwt string, errMsg string, expires time.Duration) []byte {
Expand All @@ -62,12 +63,18 @@ func serviceResponse(t *testing.T, userID string, serverID string, uJwt string,
return []byte(token)
}

func makeScopedRole(t *testing.T, role string, pub []string, sub []string) (jwt.Scope, nkeys.KeyPair) {
func newScopedRole(t *testing.T, role string, pub []string, sub []string, allowResponses bool) (*jwt.UserScope, nkeys.KeyPair) {
akp, pk := createKey(t)
r := jwt.NewUserScope()
r.Key = pk
r.Template.Sub.Allow.Add(sub...)
r.Template.Pub.Allow.Add(pub...)
if allowResponses {
r.Template.Resp = &jwt.ResponsePermission{
MaxMsgs: 1,
Expires: time.Second * 3,
}
}
r.Role = role
return r, akp
}
Expand Down Expand Up @@ -131,7 +138,7 @@ func NewAuthTest(t *testing.T, config string, authHandler nats.MsgHandler, clien
a.srv, _ = RunServerWithConfig(a.conf)

var err error
a.authClient = a.Connect(clientOptions...)
a.authClient = a.ConnectCallout(clientOptions...)
_, err = a.authClient.Subscribe(AuthCalloutSubject, authHandler)
require_NoError(t, err)
return a
Expand All @@ -146,6 +153,15 @@ func (at *authTest) NewClient(clientOptions ...nats.Option) (*nats.Conn, error)
return conn, nil
}

func (at *authTest) ConnectCallout(clientOptions ...nats.Option) *nats.Conn {
conn, err := at.NewClient(clientOptions...)
if err != nil {
err = fmt.Errorf("callout client failed: %w", err)
}
require_NoError(at.t, err)
return conn
}

func (at *authTest) Connect(clientOptions ...nats.Option) *nats.Conn {
conn, err := at.NewClient(clientOptions...)
require_NoError(at.t, err)
Expand Down Expand Up @@ -457,6 +473,7 @@ func createAuthServiceUser(t *testing.T, accKp nkeys.KeyPair) (pub, creds string
seed, _ := ukp.Seed()
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
uclaim.Name = "auth-service"
uclaim.Subject = upub
vr := jwt.ValidationResults{}
uclaim.Validate(&vr)
Expand All @@ -473,6 +490,7 @@ func createBasicAccountUser(t *testing.T, accKp nkeys.KeyPair) (creds string) {
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
uclaim.Subject = upub
uclaim.Name = "auth-client"
// For these deny all permission
uclaim.Permissions.Pub.Deny.Add(">")
uclaim.Permissions.Sub.Deny.Add(">")
Expand All @@ -484,6 +502,28 @@ func createBasicAccountUser(t *testing.T, accKp nkeys.KeyPair) (creds string) {
return genCredsFile(t, ujwt, seed)
}

func createScopedUser(t *testing.T, accKp nkeys.KeyPair, sk nkeys.KeyPair) (creds string) {
t.Helper()
ukp, _ := nkeys.CreateUser()
seed, _ := ukp.Seed()
upub, _ := ukp.PublicKey()
uclaim := newJWTTestUserClaims()
apk, _ := accKp.PublicKey()
uclaim.IssuerAccount = apk
uclaim.Subject = upub
uclaim.Name = "scoped-user"
uclaim.SetScoped(true)

// Uncomment this to set the sub limits
// uclaim.Limits.Subs = 0
vr := jwt.ValidationResults{}
uclaim.Validate(&vr)
require_Len(t, len(vr.Errors()), 0)
ujwt, err := uclaim.Encode(sk)
require_NoError(t, err)
return genCredsFile(t, ujwt, seed)
}

func TestAuthCalloutOperatorNoServerConfigCalloutAllowed(t *testing.T) {
conf := createConfFile(t, []byte(fmt.Sprintf(`
listen: 127.0.0.1:-1
Expand Down Expand Up @@ -516,7 +556,7 @@ func TestAuthCalloutOperatorModeBasics(t *testing.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.>"})
scope, scopedKp := newScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"}, false)
accClaim.SigningKeys.AddScopedSigner(scope)
accJwt, err := accClaim.Encode(oKp)
require_NoError(t, err)
Expand Down Expand Up @@ -674,6 +714,153 @@ func TestAuthCalloutOperatorModeBasics(t *testing.T) {
require_Equal(t, "foo.>", userInfo.Permissions.Subscribe.Allow[1])
}

func testAuthCalloutScopedUser(t *testing.T, allowAnyAccount bool) {
_, spub := createKey(t)
sysClaim := jwt.NewAccountClaims(spub)
sysClaim.Name = "$SYS"
sysJwt, err := sysClaim.Encode(oKp)
require_NoError(t, err)

// TEST account.
_, tpub := createKey(t)
_, tSigningPub := createKey(t)
accClaim := jwt.NewAccountClaims(tpub)
accClaim.Name = "TEST"
accClaim.SigningKeys.Add(tSigningPub)
scope, scopedKp := newScopedRole(t, "foo", []string{"foo.>", "$SYS.REQ.USER.INFO"}, []string{"foo.>", "_INBOX.>"}, true)
scope.Template.Limits.Subs = 10
scope.Template.Limits.Payload = 512
accClaim.SigningKeys.AddScopedSigner(scope)
accJwt, err := accClaim.Encode(oKp)
require_NoError(t, err)

// AUTH service account.
akp, err := nkeys.FromSeed([]byte(authCalloutIssuerSeed))
require_NoError(t, err)

apub, err := akp.PublicKey()
require_NoError(t, err)

// The authorized user for the service.
upub, creds := createAuthServiceUser(t, akp)
defer removeFile(t, creds)

authClaim := jwt.NewAccountClaims(apub)
authClaim.Name = "AUTH"
authClaim.EnableExternalAuthorization(upub)
if allowAnyAccount {
authClaim.Authorization.AllowedAccounts.Add("*")
} else {
authClaim.Authorization.AllowedAccounts.Add(tpub)
}
// the scope for the bearer token which has no permissions
sentinelScope, authKP := newScopedRole(t, "sentinel", nil, nil, false)
sentinelScope.Template.Sub.Deny.Add(">")
sentinelScope.Template.Pub.Deny.Add(">")
sentinelScope.Template.Limits.Subs = 0
sentinelScope.Template.Payload = 0
authClaim.SigningKeys.AddScopedSigner(sentinelScope)

authJwt, err := authClaim.Encode(oKp)
require_NoError(t, err)

conf := fmt.Sprintf(`
listen: 127.0.0.1:-1
operator: %s
system_account: %s
resolver: MEM
resolver_preload: {
%s: %s
%s: %s
%s: %s
}
`, ojwt, spub, apub, authJwt, tpub, accJwt, spub, sysJwt)

const scopedToken = "--Scoped--"
handler := func(m *nats.Msg) {
user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
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 {
m.Respond(nil)
}
}

ac := NewAuthTest(t, conf, handler, nats.UserCredentials(creds))
defer ac.Cleanup()
resp, err := ac.authClient.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)
expected := &UserInfo{
UserID: upub,
Account: apub,
Permissions: &Permissions{
Publish: &SubjectPermission{
Deny: []string{AuthCalloutSubject}, // Will be auto-added since in auth account.
},
Subscribe: &SubjectPermission{},
},
}
if !reflect.DeepEqual(expected, userInfo) {
t.Fatalf("User info did not match expected, expected auto-deny permissions on callout subject")
}

// Bearer token - this has no permissions see sentinelScope
// 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.
creds = createScopedUser(t, akp, authKP)
defer removeFile(t, creds)

// 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])

_, err = nc.Subscribe("foo.>", func(msg *nats.Msg) {
t.Log("got request on foo.>")
require_NoError(t, msg.Respond(nil))
})
require_NoError(t, err)

m, err := nc.Request("foo.bar", nil, time.Second)
require_NoError(t, err)
require_NotNil(t, m)
t.Log("go response from foo.bar")

nc.Close()
}

func TestAuthCalloutScopedUserAssignedAccount(t *testing.T) {
testAuthCalloutScopedUser(t, false)
}

func TestAuthCalloutScopedUserAllAccount(t *testing.T) {
testAuthCalloutScopedUser(t, true)
}

const (
curveSeed = "SXAAXMRAEP6JWWHNB6IKFL554IE6LZVT6EY5MBRICPILTLOPHAG73I3YX4"
curvePublic = "XAB3NANV3M6N7AHSQP2U5FRWKKUT7EG2ZXXABV4XVXYQRJGM4S2CZGHT"
Expand Down

0 comments on commit 3bb4807

Please sign in to comment.