Skip to content

Commit

Permalink
Added operator and user jwt/sig auth
Browse files Browse the repository at this point in the history
Signed-off-by: Derek Collison <derek@nats.io>
  • Loading branch information
derekcollison committed Mar 22, 2019
1 parent 8be3838 commit 7f69ae4
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 52 deletions.
94 changes: 93 additions & 1 deletion server/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,100 @@ func (s *Server) isGatewayAuthorized(c *client) bool {

// isLeafNodeAuthorized will check for auth for an inbound leaf node connection.
func (s *Server) isLeafNodeAuthorized(c *client) bool {
// FIXME(dlc) - This is duplicated from client auth, should be able to combine
// and not fail so bad on DRY.

// Grab under lock but process after.
var (
opts *Options
juc *jwt.UserClaims
acc *Account
err error
)

s.mu.Lock()

// Grab options
opts = s.opts

// Check if we have trustedKeys defined in the server. If so we require a user jwt.
if s.trustedKeys != nil {
if c.opts.JWT == "" {
s.mu.Unlock()
c.Debugf("Authentication requires a user JWT")
return false
}
// So we have a valid user jwt here.
juc, err = jwt.DecodeUserClaims(c.opts.JWT)
if err != nil {
s.mu.Unlock()
c.Debugf("User JWT not valid: %v", err)
return false
}
vr := jwt.CreateValidationResults()
juc.Validate(vr)
if vr.IsBlocking(true) {
s.mu.Unlock()
c.Debugf("User JWT no longer valid: %+v", vr)
return false
}
}
s.mu.Unlock()

// If we have a jwt and a userClaim, make sure we have the Account, etc associated.
// We need to look up the account. This will use an account resolver if one is present.
if juc != nil {
if acc, _ = s.LookupAccount(juc.Issuer); acc == nil {
c.Debugf("Account JWT can not be found")
return false
}
// FIXME(dlc) - Add in check for account allowed to do leaf nodes?
// Bool or active count like client?
if !s.isTrustedIssuer(acc.Issuer) {
c.Debugf("Account JWT not signed by trusted operator")
return false
}
if acc.IsExpired() {
c.Debugf("Account JWT has expired")
return false
}
// Verify the signature against the nonce.
if c.opts.Sig == "" {
c.Debugf("Signature missing")
return false
}
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
if err != nil {
// Allow fallback to normal base64.
sig, err = base64.StdEncoding.DecodeString(c.opts.Sig)
if err != nil {
c.Debugf("Signature not valid base64")
return false
}
}
pub, err := nkeys.FromPublicKey(juc.Subject)
if err != nil {
c.Debugf("User nkey not valid: %v", err)
return false
}
if err := pub.Verify(c.nonce, sig); err != nil {
c.Debugf("Signature not verified")
return false
}

nkey := buildInternalNkeyUser(juc, acc)
c.RegisterNkeyUser(nkey)

// Generate a connect event if we have a system account.
// FIXME(dlc) - Make one for leafnodes if we track active connections.
//s.accountConnectEvent(c)

// Check if we need to set an auth timer if the user jwt expires.
c.checkExpiration(juc.Claims())
return true
}

// Snapshot server options.
opts := s.getOpts()
if opts.LeafNode.Username == "" {
return true
}
Expand Down
2 changes: 1 addition & 1 deletion server/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2944,7 +2944,7 @@ func (c *client) closeConnection(reason ClosedState) {
} else if c.isSolicitedLeafNode() {
// Check if this is a solicited leaf node. Start up a reconnect.
// FIXME(dlc) - use the connectedURLs info like a normal client.
srv.startGoRoutine(func() { srv.connectToRemoteLeafNode(c.leaf.remote) })
srv.startGoRoutine(func() { srv.reConnectToRemoteLeafNode(c.leaf.remote) })
}
}

Expand Down
1 change: 1 addition & 0 deletions server/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ func (s *Server) enableAccountTracking(a *Account) {
func (s *Server) sendLeafNodeConnect(a *Account) {
s.mu.Lock()
// If we do not have any gateways defined this should also be a no-op.
// FIXME(dlc) - if we do accounting for operator limits might have to send regardless.
if a == nil || !s.eventsEnabled() || !s.gateway.enabled {
s.mu.Unlock()
return
Expand Down
104 changes: 81 additions & 23 deletions server/leafnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@ package server
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"regexp"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"

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

type leaf struct {
Expand Down Expand Up @@ -74,15 +79,26 @@ func validateLeafNode(o *Options) error {
return nil
}

func (s *Server) reConnectToRemoteLeafNode(remote *RemoteLeafOpts) {
// TODO(dlc) - We will use route delay semantics for now.
delay := DEFAULT_ROUTE_RECONNECT
select {
case <-time.After(delay):
case <-s.quitCh:
s.grWG.Done()
return
}
s.connectToRemoteLeafNode(remote)
}

func (s *Server) connectToRemoteLeafNode(remote *RemoteLeafOpts) {
defer s.grWG.Done()

if remote == nil || remote.URL == nil {
s.Debugf("Empty remote leaf node definition, can not connect")
s.Debugf("Empty remote leaf node definition, nothing to connect")
return
}

// TODO(dlc) - Auth stuff here
for s.isRunning() && s.remoteLeafNodeStillValid(remote) {
rURL := remote.URL
s.Debugf("Trying to connect as leaf node to remote server on %s", rURL.Host)
Expand Down Expand Up @@ -204,19 +220,55 @@ func (s *Server) leafNodeAcceptLoop(ch chan struct{}) {
s.done <- true
}

// RegEx to match a creds file with user JWT and Seed.
var credsRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}\n))`)

// Lock should be held entering here.
func (c *client) sendLeafConnect(tlsRequired bool) {
var user, pass string
if userInfo := c.leaf.remote.URL.User; userInfo != nil {
user = userInfo.Username()
pass, _ = userInfo.Password()
}
// We support basic user/pass and operator based user JWT with signatures.
cinfo := leafConnectInfo{
User: user,
Pass: pass,
TLS: tlsRequired,
Name: c.srv.info.ID,
}

// Check for credentials first, that will take precedence..
if creds := c.leaf.remote.Credentials; creds != "" {
c.Debugf("Authenticating with credentials file %q", c.leaf.remote.Credentials)
contents, err := ioutil.ReadFile(creds)
if err != nil {
c.Errorf("%v", err)
return
}
defer wipeSlice(contents)
items := credsRe.FindAllSubmatch(contents, -1)
if len(items) < 2 {
c.Errorf("Credentials file malformed")
return
}
// First result should be the user JWT.
// We copy here so that the file containing the seed will be wiped appropriately.
raw := items[0][1]
tmp := make([]byte, len(raw))
copy(tmp, raw)
// Seed is second item.
kp, err := nkeys.FromSeed(items[1][1])
if err != nil {
c.Errorf("Credentials file has malformed seed")
return
}
// Wipe our key on exit.
defer kp.Wipe()

sigraw, _ := kp.Sign(c.nonce)
sig := base64.RawURLEncoding.EncodeToString(sigraw)
cinfo.JWT = string(tmp)
cinfo.Sig = sig
} else if userInfo := c.leaf.remote.URL.User; userInfo != nil {
cinfo.User = userInfo.Username()
pass, _ := userInfo.Password()
cinfo.Pass = pass
}

b, err := json.Marshal(cinfo)
if err != nil {
c.Errorf("Error marshaling CONNECT to route: %v\n", err)
Expand Down Expand Up @@ -344,10 +396,10 @@ func (s *Server) createLeafNode(conn net.Conn, remote *RemoteLeafOpts) *client {
c.Debugf("Remote leaf node connect msg sent")
} else {
// Send our info to the other side.
var raw [nonceLen]byte
nonce := raw[:]
s.generateNonce(nonce)
info.Nonce = string(nonce)
// Remember the nonce we sent here for signatures, etc.
c.nonce = make([]byte, nonceLen)
s.generateNonce(c.nonce)
info.Nonce = string(c.nonce)
info.CID = c.cid
b, _ := json.Marshal(info)
pcs := [][]byte{[]byte("INFO"), b, []byte(CR_LF)}
Expand Down Expand Up @@ -400,8 +452,10 @@ func (s *Server) createLeafNode(conn net.Conn, remote *RemoteLeafOpts) *client {

c.mu.Unlock()

// Update server's accounting
s.addLeafNodeConnection(c)
// Update server's accounting here if we solicited.
if solicited {
s.addLeafNodeConnection(c)
}

return c
}
Expand Down Expand Up @@ -460,14 +514,15 @@ func (s *Server) removeLeafNodeConnection(c *client) {
}

type leafConnectInfo struct {
Nkey string `json:"nkey,omitempty"`
JWT string `json:"jwt,omitempty"`
Sig string `json:"sig,omitempty"`
User string `json:"user,omitempty"`
Pass string `json:"pass,omitempty"`
TLS bool `json:"tls_required"`
Comp bool `json:"compression,omitempty"`
Name string `json:"name,omitempty"`
JWT string `json:"jwt,omitempty"`
Sig string `json:"sig,omitempty"`
User string `json:"user,omitempty"`
Pass string `json:"pass,omitempty"`
TLS bool `json:"tls_required"`
Comp bool `json:"compression,omitempty"`
Name string `json:"name,omitempty"`

// Just used to detect wrong connection attempts.
Gateway string `json:"gateway,omitempty"`
}

Expand Down Expand Up @@ -514,6 +569,9 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
s.grWG.Done()
})

// Add in the leafnode here since we passed through auth at this point.
s.addLeafNodeConnection(c)

// Announce the account connect event for a leaf node.
// This will no-op as needed.
s.sendLeafNodeConnect(c.acc)
Expand Down

0 comments on commit 7f69ae4

Please sign in to comment.