Skip to content

Commit

Permalink
bumped to latest version of jwt branch
Browse files Browse the repository at this point in the history
removed server_key as name/id are already the right values
  • Loading branch information
aricart committed Mar 16, 2023
2 parents 805544c + 9f69d87 commit 247f3b9
Show file tree
Hide file tree
Showing 35 changed files with 2,961 additions and 1,070 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
@@ -0,0 +1 @@
* @nats-io/server
2 changes: 0 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Expand Up @@ -13,5 +13,3 @@ Resolves #
-
-
-

/cc @nats-io/core
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -9,7 +9,7 @@ language: go
go:
# This should be quoted or use .x, but should not be unquoted.
# Remember that a YAML bare float drops trailing zeroes.
- '1.19.5'
- '1.19.6'

addons:
apt:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -37,8 +37,8 @@ If you are interested in contributing to NATS, read about our...
[Fossa-Image]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Fnats-io%2Fnats-server.svg?type=shield
[Build-Status-Url]: https://travis-ci.com/github/nats-io/nats-server
[Build-Status-Image]: https://travis-ci.com/nats-io/nats-server.svg?branch=main
[Release-Url]: https://github.com/nats-io/nats-server/releases/tag/v2.9.14
[Release-image]: https://img.shields.io/badge/release-v2.9.14-1eb0fc.svg
[Release-Url]: https://github.com/nats-io/nats-server/releases/tag/v2.9.15
[Release-image]: https://img.shields.io/badge/release-v2.9.15-1eb0fc.svg
[Coverage-Url]: https://coveralls.io/r/nats-io/nats-server?branch=main
[Coverage-image]: https://coveralls.io/repos/github/nats-io/nats-server/badge.svg?branch=main
[ReportCard-Url]: https://goreportcard.com/report/nats-io/nats-server
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Expand Up @@ -3,10 +3,10 @@ module github.com/nats-io/nats-server/v2
go 1.19

require (
github.com/klauspost/compress v1.15.15
github.com/klauspost/compress v1.16.0
github.com/minio/highwayhash v1.0.2
github.com/nats-io/jwt/v2 v2.3.1-0.20230217195706-f72ebb5b11ab
github.com/nats-io/nats.go v1.23.0
github.com/nats-io/jwt/v2 v2.3.1-0.20230313184310-f333f8d960ea
github.com/nats-io/nats.go v1.24.0
github.com/nats-io/nkeys v0.3.1-0.20221215194120-47c7408e7546
github.com/nats-io/nuid v1.0.1
go.uber.org/automaxprocs v1.5.1
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Expand Up @@ -9,14 +9,16 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/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.20230217195706-f72ebb5b11ab h1:hrC04x1cRy8lYecBOBlFRIp33R+9u88qmw98gAJDO9s=
github.com/nats-io/jwt/v2 v2.3.1-0.20230217195706-f72ebb5b11ab/go.mod h1:TMyHPxJBtWU3E8xIeLl4xAWBa5l3fLpIkbH0QA2ce1o=
github.com/nats-io/nats.go v1.23.0 h1:lR28r7IX44WjYgdiKz9GmUeW0uh/m33uD3yEjLZ2cOE=
github.com/nats-io/nats.go v1.23.0/go.mod h1:ki/Scsa23edbh8IRZbCuNXR9TDcbvfaSijKtaqQgw+Q=
github.com/nats-io/jwt/v2 v2.3.1-0.20230313184310-f333f8d960ea h1:6ZnbJsBGtdoZrPDTPER8qo+Ukbym7lefz6SYW/tF9AY=
github.com/nats-io/jwt/v2 v2.3.1-0.20230313184310-f333f8d960ea/go.mod h1:NVnZxUw7wM29R3IzeeMgAWFWm6gF5QCnWXu3l+JAHMg=
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/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
Expand Down
14 changes: 11 additions & 3 deletions server/accounts.go
Expand Up @@ -2313,7 +2313,9 @@ func (a *Account) addRespServiceImport(dest *Account, to string, osi *serviceImp

// Always grab time and make sure response threshold timer is running.
si.ts = time.Now().UnixNano()
osi.se.setResponseThresholdTimer()
if osi.se != nil {
osi.se.setResponseThresholdTimer()
}

if rt == Singleton && tracking {
si.latency = osi.latency
Expand Down Expand Up @@ -3922,7 +3924,10 @@ func (dr *DirAccResolver) Start(s *Server) error {
return fmt.Errorf("error setting up update handling: %v", err)
}
}
if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, _ *client, _ *Account, subj, resp string, msg []byte) {
if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, c *client, _ *Account, _, resp string, msg []byte) {
// As this is a raw message, we need to extract payload and only decode claims from it,
// in case request is sent with headers.
_, msg = c.msgParts(msg)
if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil {
respondToUpdate(s, resp, "n/a", "jwt update resulted in error", err)
} else if claim.Issuer == op && strict {
Expand Down Expand Up @@ -4210,7 +4215,10 @@ func (dr *CacheDirAccResolver) Start(s *Server) error {
return fmt.Errorf("error setting up update handling: %v", err)
}
}
if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, _ *client, _ *Account, subj, resp string, msg []byte) {
if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, c *client, _ *Account, _, resp string, msg []byte) {
// As this is a raw message, we need to extract payload and only decode claims from it,
// in case request is sent with headers.
_, msg = c.msgParts(msg)
if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil {
respondToUpdate(s, resp, "n/a", "jwt update cache resulted in error", err)
} else if claim.Issuer == op && strict {
Expand Down
138 changes: 45 additions & 93 deletions server/auth_callout.go
Expand Up @@ -14,46 +14,23 @@
package server

import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"

"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)

const (
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
AuthRequestSubject = "nats-authorization-request"
AuthRequestXKeyHeader = "Nats-Server-Xkey"
AuthCalloutServerIdTag = "Auth-Callout-Server-ID"
AuthAccountPKHeader = "Auth-Account-Pk"
AuthAccountSigHeader = "Auth-Account-Sig"
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
AuthRequestSubject = "nats-authorization-request"
AuthRequestXKeyHeader = "Nats-Server-Xkey"
)

type CalloutResponse struct {
Error string `json:"error,omitempty"`
UserToken string `json:"user_token,omitempty"`
}

func getTagValue(uc *jwt.UserClaims, key string) string {
// we are expecting `tag: serverID` or `tag:serverID`
// note that tags lower case
k := fmt.Sprintf("%s:", strings.ToLower(key))
for _, t := range uc.Tags {
if strings.HasPrefix(t, k) {
return t[len(k):]
}
}
// not found
return ""
}

// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
isOperatorMode := len(opts.TrustedKeys) > 0
Expand Down Expand Up @@ -101,10 +78,7 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize

decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
account := acc.Name
hdr, msg := rc.msgParts(rmsg)
if len(hdr) == 0 {
return nil, errors.New("Auth callout violation - expected headers")
}
_, msg := rc.msgParts(rmsg)

// This signals not authorized.
// Since this is an account subscription will always have "\r\n".
Expand All @@ -115,9 +89,8 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
msg = msg[:len(msg)-LEN_CR_LF]
encrypted := false
// If we sent an encrypted request the response could be encrypted as well.
// we are expecting JSON, so first char is `{`
// possibly faster to do bytes.HasPrefix(msg, []byte("{"))
if xkp != nil && len(msg) > 0 && !json.Valid(msg) {
// 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 {
Expand All @@ -126,65 +99,45 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
encrypted = true
}

ar := CalloutResponse{}
if err := json.Unmarshal(msg, &ar); err != nil {
return nil, fmt.Errorf("Auth callout violation: error deserializing JSON: %v", err)
cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
if err != nil {
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")
}

// 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 := string(getHeader(AuthAccountPKHeader, hdr))
if pkStr == "" {
return nil, errors.New("Auth callout violation - expected auth account pk")
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")
}
}
pk, err := nkeys.FromPublicKey(pkStr)
if err != nil {
return nil, fmt.Errorf("Error parsing auth key: %v", err)
}
sig := string(getHeader(AuthAccountSigHeader, hdr))
if sig == "" {
return nil, errors.New("Auth callout violation - expected auth sig")
}
dSig, err := base64.RawURLEncoding.DecodeString(sig)
if err != nil {
return nil, fmt.Errorf("Error decoding sig: %v", err)
}
// verify that the payload we got, is signed by the auth account
if err := pk.Verify(msg, dSig); err != nil {
return nil, fmt.Errorf("Unable to validate sig: %v", err)
}
}

if len(ar.Error) > 0 {
return nil, fmt.Errorf("Auth callout violation: %q", ar.Error)
}

// if we got a message - we got a JWT, the authentication shouldn't be yielding JWTs on failure
return jwt.DecodeUserClaims(ar.UserToken)
}

// FIXME: we were making a point of checking the server id, but, this is not really necessary
// because the user public key, has to be the one that we generated so all this code here
// is not necessary and complicates the response - refactored here so that we can zap
// checkServerID validates the server ID matches a value sent to the callout
// and expected to be stated in one of the tags
checkServerID := func(arc *jwt.UserClaims, expectedServerID string) error {
serverID := getTagValue(arc, AuthCalloutServerIdTag)
if serverID == "" {
return fmt.Errorf("Auth callout violation: missing server id on account %q", expectedServerID)
}
// Require the response to have pinned the audience to this server.
// but the server ID will be lowercase
if serverID != strings.ToLower(s.info.ID) {
return fmt.Errorf("Wrong server id received for auth callout response on account %q, expected %q got %q",
expectedServerID, s.info.ID, serverID)
}
return nil
return jwt.DecodeUserClaims(cr.Jwt)
}

// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
Expand All @@ -201,21 +154,21 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize

// the jwt issuer can be a signing key
jwtIssuer := arc.Issuer
if arc.IssuerAccount != "" {
if arc.IssuerAccount != _EMPTY_ {
if !isOperatorMode {
// 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 "", fmt.Errorf("Error non operator mode account %q: attempted to use issuer_account", account)
return _EMPTY_, fmt.Errorf("Error non operator mode account %q: attempted to use issuer_account", account)
}
jwtIssuer = arc.IssuerAccount
}

if jwtIssuer != issuer {
if !isOperatorMode {
return "", fmt.Errorf("Wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
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 "", fmt.Errorf("Account %q not permitted as valid account option for auth callout for account %q",
return _EMPTY_, fmt.Errorf("Account %q not permitted as valid account option for auth callout for account %q",
arc.Issuer, account)
}
}
Expand All @@ -236,7 +189,6 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
return 0, nil, fmt.Errorf("Authorized user on account %q using invalid connection type", account)
}
}

return expiration, allowedConnTypes, nil
}

Expand All @@ -249,7 +201,7 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
}

// if we are not in operator mode, they can specify placement as a tag
placement := ""
placement := _EMPTY_
if !isOperatorMode {
// only allow placement if we are not in operator mode
placement = arc.Audience
Expand Down Expand Up @@ -287,19 +239,19 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
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
}

// FIXME: this is likely not necessary - user ID is already the nonce
if err := checkServerID(arc, racc.Name); err != nil {
respCh <- err.Error()
return
}

expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
if err != nil {
respCh <- err.Error()
Expand Down

0 comments on commit 247f3b9

Please sign in to comment.