Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify authresponse #3864

Merged
merged 4 commits into from Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions go.mod
Expand Up @@ -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
)

Expand Down
18 changes: 10 additions & 8 deletions go.sum
Expand Up @@ -11,26 +11,28 @@ 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=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
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=
Expand Down
225 changes: 164 additions & 61 deletions server/auth_callout.go
Expand Up @@ -17,6 +17,7 @@ import (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"time"

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand All @@ -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_
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down