Skip to content

Commit

Permalink
Cherry-picks for 2.10.17 (#5565)
Browse files Browse the repository at this point in the history
Includes the following:

* #5555 
* #5561

Signed-off-by: Neil Twigg <[email protected]>
  • Loading branch information
neilalexander authored Jun 19, 2024
2 parents 4efcf49 + 1d94d2c commit d3dae63
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 1 deletion.
31 changes: 30 additions & 1 deletion server/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3593,6 +3593,14 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
}
c.mu.Lock()
c.applyAccountLimits()
// if we have an nkey user we are a callout user - save
// the issuedAt, and nkey user id to honor revocations
var nkeyUserID string
var issuedAt int64
if c.user != nil {
issuedAt = c.user.Issued
nkeyUserID = c.user.Nkey
}
theJWT := c.opts.JWT
c.mu.Unlock()
// Check for being revoked here. We use ac one to avoid the account lock.
Expand All @@ -3611,6 +3619,27 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
continue
}
}

// if we extracted nkeyUserID and issuedAt we are a callout type
// calloutIAT should only be set if we are in callout scenario as
// the user JWT is _NOT_ associated with the client for callouts,
// so we rely on the calloutIAT to know when the JWT was issued
// revocations simply state that JWT issued before or by that date
// are not valid
if ac.Revocations != nil && nkeyUserID != _EMPTY_ && issuedAt > 0 {
seconds, ok := ac.Revocations[jwt.All]
if ok && seconds >= issuedAt {
c.sendErrAndDebug("User Authentication Revoked")
c.closeConnection(Revocation)
continue
}
seconds, ok = ac.Revocations[nkeyUserID]
if ok && seconds >= issuedAt {
c.sendErrAndDebug("User Authentication Revoked")
c.closeConnection(Revocation)
continue
}
}
}

// Check if the signing keys changed, might have to evict
Expand Down Expand Up @@ -3719,7 +3748,7 @@ func buildPermissionsFromJwt(uc *jwt.Permissions) *Permissions {

// Helper to build internal NKeyUser.
func buildInternalNkeyUser(uc *jwt.UserClaims, acts map[string]struct{}, acc *Account) *NkeyUser {
nu := &NkeyUser{Nkey: uc.Subject, Account: acc, AllowedConnectionTypes: acts}
nu := &NkeyUser{Nkey: uc.Subject, Account: acc, AllowedConnectionTypes: acts, Issued: uc.IssuedAt}
if uc.IssuerAccount != _EMPTY_ {
nu.SigningKey = uc.Issuer
}
Expand Down
1 change: 1 addition & 0 deletions server/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type ClientAuthentication interface {
// NkeyUser is for multiple nkey based users
type NkeyUser struct {
Nkey string `json:"user"`
Issued int64 `json:"issued,omitempty"` // this is a copy of the issued at (iat) field in the jwt
Permissions *Permissions `json:"permissions,omitempty"`
Account *Account `json:"account,omitempty"`
SigningKey string `json:"signing_key,omitempty"`
Expand Down
173 changes: 173 additions & 0 deletions server/auth_callout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import (
"encoding/pem"
"errors"
"fmt"
"os"
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -1827,3 +1829,174 @@ func TestAuthCallout_ClientAuthErrorOperatorMode(t *testing.T) {
testAuthCall_ClientAuthErrorOperatorMode(t, true)
testAuthCall_ClientAuthErrorOperatorMode(t, false)
}

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

// TEST account.
tkp, tpub := createKey(t)
accClaim := jwt.NewAccountClaims(tpub)
accClaim.Name = "TEST"
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)
authClaim.Authorization.AllowedAccounts.Add(tpub)
authJwt, err := authClaim.Encode(oKp)
require_NoError(t, err)

jwtDir, err := os.MkdirTemp("", "")
require_NoError(t, err)
defer func() {
_ = os.RemoveAll(jwtDir)
}()

conf := fmt.Sprintf(`
listen: 127.0.0.1:-1
operator: %s
system_account: %s
resolver: {
type: "full"
dir: %s
allow_delete: false
interval: "2m"
timeout: "1.9s"
}
resolver_preload: {
%s: %s
%s: %s
%s: %s
}
`, ojwt, spub, jwtDir, apub, authJwt, tpub, accJwt, spub, sysJwt)

const token = "--secret--"

users := make(map[string]string)
handler := func(m *nats.Msg) {
user, si, _, opts, _ := decodeAuthRequest(t, m.Data)
if opts.Token == token {
// must have no limits set
ujwt := createAuthUser(t, user, "user", tpub, tpub, tkp, 0, &jwt.UserPermissionLimits{})
users[opts.Name] = ujwt
m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
} else {
m.Respond(nil)
}
}

ac := NewAuthTest(t, conf, handler, nats.UserCredentials(creds))
defer ac.Cleanup()

// create a system user
_, sysCreds := createAuthServiceUser(t, skp)
defer removeFile(t, sysCreds)
// connect the system user
sysNC, err := ac.NewClient(nats.UserCredentials(sysCreds))
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.
creds = createBasicAccountUser(t, akp)
defer removeFile(t, creds)

var fwg sync.WaitGroup

// connect three clients
nc := ac.Connect(nats.UserCredentials(creds), nats.Name("first"), nats.Token(token), nats.NoReconnect(), nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
if err != nil && strings.Contains(err.Error(), "authentication revoked") {
fwg.Done()
}
}))
fwg.Add(1)

var swg sync.WaitGroup
// connect another user
ncA := ac.Connect(nats.UserCredentials(creds), nats.Token(token), nats.Name("second"), nats.NoReconnect(), nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
if err != nil && strings.Contains(err.Error(), "authentication revoked") {
swg.Done()
}
}))
swg.Add(1)

ncB := ac.Connect(nats.UserCredentials(creds), nats.Token(token), nats.NoReconnect(), nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) {
if err != nil && strings.Contains(err.Error(), "authentication revoked") {
swg.Done()
}
}))
swg.Add(1)

require_NoError(t, err)

// revoke the user first - look at the JWT we issued
uc, err := jwt.DecodeUserClaims(users["first"])
require_NoError(t, err)
// revoke the user in account
accClaim.Revocations = make(map[string]int64)
accClaim.Revocations.Revoke(uc.Subject, time.Now().Add(time.Minute))
accJwt, err = accClaim.Encode(oKp)
require_NoError(t, err)
// send the update request
updateAccount(t, sysNC, accJwt)

// wait for the user to be disconnected with the error we expect
fwg.Wait()
require_Equal(t, nc.IsConnected(), false)

// update the account to remove any revocations
accClaim.Revocations = make(map[string]int64)
accJwt, err = accClaim.Encode(oKp)
require_NoError(t, err)
updateAccount(t, sysNC, accJwt)
// we should still be connected on the other 2 clients
require_Equal(t, ncA.IsConnected(), true)
require_Equal(t, ncB.IsConnected(), true)

// update the jwt and revoke all users
accClaim.Revocations.Revoke(jwt.All, time.Now().Add(time.Minute))
accJwt, err = accClaim.Encode(oKp)
require_NoError(t, err)
updateAccount(t, sysNC, accJwt)

swg.Wait()
require_Equal(t, ncA.IsConnected(), false)
require_Equal(t, ncB.IsConnected(), false)
}

func updateAccount(t *testing.T, sys *nats.Conn, jwtToken string) {
ac, err := jwt.DecodeAccountClaims(jwtToken)
require_NoError(t, err)
r, err := sys.Request(fmt.Sprintf(`$SYS.REQ.ACCOUNT.%s.CLAIMS.UPDATE`, ac.Subject), []byte(jwtToken), time.Second*2)
require_NoError(t, err)

type data struct {
Account string `json:"account"`
Code int `json:"code"`
}
type serverResponse struct {
Data data `json:"data"`
}

var response serverResponse
err = json.Unmarshal(r.Data, &response)
require_NoError(t, err)
require_NotNil(t, response.Data)
require_Equal(t, response.Data.Code, int(200))
}

0 comments on commit d3dae63

Please sign in to comment.